Merge pull request #21 from StudyLions/staging

Top.gg module and general bugfixes
This commit is contained in:
Interitio
2022-01-20 23:16:37 +02:00
committed by GitHub
45 changed files with 845 additions and 140 deletions

View File

@@ -31,7 +31,7 @@ This feature allows the users to use their coins to schedule a time to study at.
Not attending prevents everyone in the room from getting the bonus.
- **Study and Work Statistics**
Users can view their daily, weekly, monthly and all-time stats, as well as their study streak.
- `Coming Soon` **Pomodoro Timers**
- **Pomodoro Timers**
The bot will show the timer in the title of the study room and play a sound at the start and end of each session.
- **Private Study Rooms**
Allows the members to create their own private study rooms and invite their friends to join!

88
bot/LionContext.py Normal file
View File

@@ -0,0 +1,88 @@
import types
from cmdClient import Context
from cmdClient.logger import log
class LionContext(Context):
"""
Subclass to allow easy attachment of custom hooks and structure to contexts.
"""
__slots__ = ()
@classmethod
def util(cls, util_func):
"""
Decorator to make a utility function available as a Context instance method.
Extends the default Context method to add logging and to return the utility function.
"""
super().util(util_func)
log(f"Attached context utility function: {util_func.__name__}")
return util_func
@classmethod
def wrappable_util(cls, util_func):
"""
Decorator to add a Wrappable utility function as a Context instance method.
"""
wrappable = Wrappable(util_func)
super().util(wrappable)
log(f"Attached wrappable context utility function: {util_func.__name__}")
return wrappable
class Wrappable:
__slots__ = ('_func', 'wrappers')
def __init__(self, func):
self._func = func
self.wrappers = None
@property
def __name__(self):
return self._func.__name__
def add_wrapper(self, func, name=None):
self.wrappers = self.wrappers or {}
name = name or func.__name__
self.wrappers[name] = func
log(
f"Added wrapper '{name}' to Wrappable '{self._func.__name__}'.",
context="Wrapping"
)
def remove_wrapper(self, name):
if not self.wrappers or name not in self.wrappers:
raise ValueError(
f"Cannot remove non-existent wrapper '{name}' from Wrappable '{self._func.__name__}'"
)
self.wrappers.pop(name)
log(
f"Removed wrapper '{name}' from Wrappable '{self._func.__name__}'.",
context="Wrapping"
)
def __call__(self, *args, **kwargs):
if self.wrappers:
return self._wrapped(iter(self.wrappers.values()))(*args, **kwargs)
else:
return self._func(*args, **kwargs)
def _wrapped(self, iter_wraps):
next_wrap = next(iter_wraps, None)
if next_wrap:
def _func(*args, **kwargs):
return next_wrap(self._wrapped(iter_wraps), *args, **kwargs)
else:
_func = self._func
return _func
def __get__(self, instance, cls=None):
if instance is None:
return self
else:
return types.MethodType(self, instance)
# Override the original Context.reply with a wrappable utility
reply = LionContext.wrappable_util(Context.reply)

View File

@@ -105,6 +105,9 @@ class LionModule(Module):
await ctx.reply("I need permission to send embeds in this channel before I can run any commands!")
raise SafeCancellation(details='I cannot send embeds in this channel.')
# Ensure Lion exists and cached data is up to date
ctx.alion.update_saved_data(ctx.author)
# Start typing
await ctx.ch.trigger_typing()

View File

@@ -1,2 +1,2 @@
CONFIG_FILE = "config/bot.conf"
DATA_VERSION = 8
DATA_VERSION = 9

View File

@@ -14,7 +14,7 @@ meta = RowTable(
user_config = RowTable(
'user_config',
('userid', 'timezone'),
('userid', 'timezone', 'topgg_vote_reminder', 'avatar_hash'),
'userid',
cache=TTLCache(5000, ttl=60*5)
)
@@ -33,7 +33,8 @@ guild_config = RowTable(
'video_studyban', 'video_grace_period',
'greeting_channel', 'greeting_message', 'returning_message',
'starting_funds', 'persist_roles',
'pomodoro_channel'),
'pomodoro_channel',
'name'),
'guildid',
cache=TTLCache(2500, ttl=60*5)
)
@@ -51,6 +52,7 @@ lions = RowTable(
'revision_mute_count',
'last_study_badgeid',
'video_warned',
'display_name',
'_timestamp'
),
('guildid', 'userid'),

View File

@@ -1,4 +1,6 @@
import pytz
import discord
from functools import reduce
from datetime import datetime, timedelta
from meta import client
@@ -20,6 +22,9 @@ class Lion:
# Lion cache. Currently lions don't expire
_lions = {} # (guildid, userid) -> Lion
# Extra methods supplying an economy bonus
_economy_bonuses = []
def __init__(self, guildid, userid):
self.guildid = guildid
self.userid = userid
@@ -44,6 +49,8 @@ class Lion:
# TODO: Debug log
lion = tb.lions.fetch(key)
if not lion:
tb.user_config.fetch_or_create(userid)
tb.guild_config.fetch_or_create(guildid)
tb.lions.create_row(
guildid=guildid,
userid=userid,
@@ -71,10 +78,24 @@ class Lion:
@property
def data(self):
"""
The Row corresponding to this user.
The Row corresponding to this member.
"""
return tb.lions.fetch(self.key)
@property
def user_data(self):
"""
The Row corresponding to this user.
"""
return tb.user_config.fetch_or_create(self.userid)
@property
def guild_data(self):
"""
The Row corresponding to this guild.
"""
return tb.guild_config.fetch_or_create(self.guildid)
@property
def settings(self):
"""
@@ -120,6 +141,24 @@ class Lion:
return int(coins)
@property
def economy_bonus(self):
"""
Economy multiplier
"""
return reduce(
lambda x, y: x * y,
[func(self) for func in self._economy_bonuses]
)
@classmethod
def register_economy_bonus(cls, func):
cls._economy_bonuses.append(func)
@classmethod
def unregister_economy_bonus(cls, func):
cls._economy_bonuses.remove(func)
@property
def session(self):
"""
@@ -207,6 +246,33 @@ class Lion:
return remaining
@property
def name(self):
"""
Returns the best local name possible.
"""
if self.member:
name = self.member.display_name
elif self.data.display_name:
name = self.data.display_name
else:
name = str(self.userid)
return name
def update_saved_data(self, member: discord.Member):
"""
Update the stored discord data from the givem member.
Intended to be used when we get member data from events that may not be available in cache.
"""
if self.guild_data.name != member.guild.name:
self.guild_data.name = member.guild.name
if self.user_data.avatar_hash != member.avatar:
self.user_data.avatar_hash = member.avatar
if self.data.display_name != member.display_name:
self.data.display_name = member.display_name
def localize(self, naive_utc_dt):
"""
Localise the provided naive UTC datetime into the user's timezone.
@@ -214,11 +280,11 @@ class Lion:
timezone = self.settings.timezone.value
return naive_utc_dt.replace(tzinfo=pytz.UTC).astimezone(timezone)
def addCoins(self, amount, flush=True):
def addCoins(self, amount, flush=True, ignorebonus=False):
"""
Add coins to the user, optionally store the transaction in pending.
"""
self._pending_coins += amount
self._pending_coins += amount * (1 if ignorebonus else self.economy_bonus)
self._pending[self.key] = self
if flush:
self.flush()

View File

@@ -69,6 +69,8 @@ def _format_selectkeys(keys):
"""
if not keys:
return "*"
elif type(keys) is str:
return keys
else:
return ", ".join(keys)

View File

@@ -3,7 +3,7 @@ from cmdClient.cmdClient import cmdClient
from .config import conf
from .sharding import shard_number, shard_count
from LionContext import LionContext
# Initialise client
owners = [int(owner) for owner in conf.bot.getlist('owners')]
@@ -14,6 +14,7 @@ client = cmdClient(
owners=owners,
intents=intents,
shard_id=shard_number,
shard_count=shard_count
shard_count=shard_count,
baseContext=LionContext
)
client.conf = conf

View File

@@ -1,8 +1,38 @@
from discord import PartialEmoji
import configparser as cfgp
from .args import args
class configEmoji(PartialEmoji):
__slots__ = ('fallback',)
def __init__(self, *args, fallback=None, **kwargs):
super().__init__(*args, **kwargs)
self.fallback = fallback
@classmethod
def from_str(cls, emojistr: str):
"""
Parses emoji strings of one of the following forms
`<a:name:id> or fallback`
`<:name:id> or fallback`
`<a:name:id>`
`<:name:id>`
"""
splits = emojistr.rsplit(' or ', maxsplit=1)
fallback = splits[1] if len(splits) > 1 else None
emojistr = splits[0].strip('<> ')
animated, name, id = emojistr.split(':')
return cls(
name=name,
fallback=PartialEmoji(name=fallback),
animated=bool(animated),
id=int(id)
)
class Conf:
def __init__(self, configfile, section_name="DEFAULT"):
self.configfile = configfile
@@ -11,6 +41,7 @@ class Conf:
converters={
"intlist": self._getintlist,
"list": self._getlist,
"emoji": configEmoji.from_str,
}
)
self.config.read(configfile)
@@ -20,6 +51,7 @@ class Conf:
self.default = self.config["DEFAULT"]
self.section = self.config[self.section_name]
self.bot = self.section
self.emojis = self.config['EMOJIS'] if 'EMOJIS' in self.config else self.section
# Config file recursion, read in configuration files specified in every "ALSO_READ" key.
more_to_read = self.section.getlist("ALSO_READ", [])

View File

@@ -7,6 +7,7 @@ from .stats import *
from .user_config import *
from .workout import *
from .todo import *
from .topgg import *
from .reminders import *
from .renting import *
from .moderation import *

View File

@@ -99,7 +99,7 @@ class TimeSlot:
colour=discord.Colour.orange(),
timestamp=self.start_time
).set_footer(
text="About to start!\nJoin the session with {}rooms book".format(client.prefix)
text="About to start!\nJoin the session with {}schedule book".format(client.prefix)
)
if self.members:
@@ -111,7 +111,7 @@ class TimeSlot:
)
)
else:
embed.description = "No members booked for this session!"
embed.description = "No members scheduled this session!"
return embed
@@ -125,7 +125,7 @@ class TimeSlot:
description="Finishing <t:{}:R>.".format(timestamp + 3600),
colour=discord.Colour.orange(),
timestamp=self.start_time
).set_footer(text="Join the next session using {}rooms book".format(client.prefix))
).set_footer(text="Join the next session using {}schedule book".format(client.prefix))
if self.members:
classifications = {
@@ -158,7 +158,7 @@ class TimeSlot:
if value:
embed.add_field(name=field, value='\n'.join(value))
else:
embed.description = "No members booked for this session!"
embed.description = "No members scheduled this session!"
return embed
@@ -210,7 +210,7 @@ class TimeSlot:
if value:
embed.add_field(name=field, value='\n'.join(value))
else:
embed.description = "No members booked this session!"
embed.description = "No members scheduled this session!"
return embed
@@ -316,13 +316,13 @@ class TimeSlot:
if self.data and not self.channel:
try:
self.channel = await self.guild.create_voice_channel(
"Upcoming Accountability Study Room",
"Upcoming Scheduled Session",
overwrites=overwrites,
category=self.category
)
except discord.HTTPException:
GuildSettings(self.guild.id).event_log.log(
"Failed to create the accountability voice channel. Skipping this session.",
"Failed to create the scheduled session voice channel. Skipping this session.",
colour=discord.Colour.red()
)
return None
@@ -337,7 +337,7 @@ class TimeSlot:
)
except discord.HTTPException:
GuildSettings(self.guild.id).event_log.log(
"Failed to post the status message in the accountability lobby {}.\n"
"Failed to post the status message in the scheduled session lobby {}.\n"
"Skipping this session.".format(self.lobby.mention),
colour=discord.Colour.red()
)
@@ -351,7 +351,7 @@ class TimeSlot:
Ghost pings the session members in the lobby channel.
"""
if self.members:
content = content or "Your accountability session has opened! Please join!"
content = content or "Your scheduled session has started! Please join!"
out = "{}\n\n{}".format(
content,
' '.join('<@{}>'.format(memid) for memid, mem in self.members.items() if not mem.has_attended)
@@ -366,7 +366,7 @@ class TimeSlot:
"""
if self.channel:
try:
await self.channel.edit(name="Accountability Study Room")
await self.channel.edit(name="Scheduled Session Room")
await self.channel.set_permissions(self.guild.default_role, view_channel=True, connect=False)
except discord.HTTPException:
pass
@@ -388,7 +388,7 @@ class TimeSlot:
await asyncio.sleep(delay)
embed = discord.Embed(
title="Your accountability session has started!",
title="The scheduled session you booked has started!",
description="Please join {}.".format(self.channel.mention),
colour=discord.Colour.orange()
).set_footer(

View File

@@ -9,19 +9,19 @@ from .tracker import AccountabilityGuild as AG
@GuildSettings.attach_setting
class accountability_category(settings.Channel, settings.GuildSetting):
category = "Accountability Rooms"
category = "Scheduled Sessions"
attr_name = "accountability_category"
_data_column = "accountability_category"
display_name = "accountability_category"
desc = "Category in which to make the accountability rooms."
display_name = "session_category"
desc = "Category in which to make the scheduled session rooms."
_default = None
long_desc = (
"\"Accountability\" category channel.\n"
"The accountability voice channels will be created here."
"\"Schedule session\" category channel.\n"
"Scheduled sessions will be held in voice channels created under this category."
)
_accepts = "A category channel."
@@ -33,9 +33,9 @@ class accountability_category(settings.Channel, settings.GuildSetting):
# TODO Move this somewhere better
if self.id not in AG.cache:
AG(self.id)
return "The accountability category has been changed to **{}**.".format(self.value.name)
return "The session category has been changed to **{}**.".format(self.value.name)
else:
return "The accountability system has been started in **{}**.".format(self.value.name)
return "The scheduled session system has been started in **{}**.".format(self.value.name)
else:
if self.id in AG.cache:
aguild = AG.cache.pop(self.id)
@@ -43,26 +43,26 @@ class accountability_category(settings.Channel, settings.GuildSetting):
asyncio.create_task(aguild.current_slot.cancel())
if aguild.upcoming_slot:
asyncio.create_task(aguild.upcoming_slot.cancel())
return "The accountability system has been shut down."
return "The scheduled session system has been shut down."
else:
return "The accountability category has been unset."
return "The scheduled session category has been unset."
@GuildSettings.attach_setting
class accountability_lobby(settings.Channel, settings.GuildSetting):
category = "Accountability Rooms"
category = "Scheduled Sessions"
attr_name = "accountability_lobby"
_data_column = attr_name
display_name = attr_name
desc = "Category in which to post accountability session status updates."
display_name = "session_lobby"
desc = "Category in which to post scheduled session notifications updates."
_default = None
long_desc = (
"Accountability session updates will be posted here, and members will be notified in this channel.\n"
"The channel will be automatically created in the accountability category if it does not exist.\n"
"Scheduled session updates will be posted here, and members will be notified in this channel.\n"
"The channel will be automatically created in the configured `session_category` if it does not exist.\n"
"Members do not need to be able to write in the channel."
)
_accepts = "Any text channel."
@@ -76,65 +76,65 @@ class accountability_lobby(settings.Channel, settings.GuildSetting):
@GuildSettings.attach_setting
class accountability_price(settings.Integer, GuildSetting):
category = "Accountability Rooms"
category = "Scheduled Sessions"
attr_name = "accountability_price"
_data_column = attr_name
display_name = attr_name
desc = "Cost of booking an accountability time slot."
display_name = "session_price"
desc = "Cost of booking a scheduled session."
_default = 100
long_desc = (
"The price of booking each one hour accountability room slot."
"The price of booking each one hour scheduled session slot."
)
_accepts = "An integer number of coins."
@property
def success_response(self):
return "Accountability slots now cost `{}` coins.".format(self.value)
return "Scheduled session slots now cost `{}` coins.".format(self.value)
@GuildSettings.attach_setting
class accountability_bonus(settings.Integer, GuildSetting):
category = "Accountability Rooms"
category = "Scheduled Sessions"
attr_name = "accountability_bonus"
_data_column = attr_name
display_name = attr_name
desc = "Bonus given when all accountability members attend a time slot."
display_name = "session_bonus"
desc = "Bonus given when everyone attends a scheduled session slot."
_default = 1000
long_desc = (
"The extra bonus given when all the members who have booked an accountability time slot attend."
"The extra bonus given to each scheduled session member when everyone who booked attended the session."
)
_accepts = "An integer number of coins."
@property
def success_response(self):
return "Accountability members will now get `{}` coins if everyone joins.".format(self.value)
return "Scheduled session members will now get `{}` coins if everyone joins.".format(self.value)
@GuildSettings.attach_setting
class accountability_reward(settings.Integer, GuildSetting):
category = "Accountability Rooms"
category = "Scheduled Sessions"
attr_name = "accountability_reward"
_data_column = attr_name
display_name = attr_name
desc = "Reward given for attending a booked accountability slot."
display_name = "session_reward"
desc = "The individual reward given when a member attends their booked scheduled session."
_default = 200
long_desc = (
"Amount given to a member who books an accountability slot and attends it."
"Reward given to a member who attends a booked scheduled session."
)
_accepts = "An integer number of coins."
@property
def success_response(self):
return "Accountability members will now get `{}` coins at the end of their slot.".format(self.value)
return "Members will now get `{}` coins when they attend their scheduled session.".format(self.value)

View File

@@ -62,28 +62,29 @@ def ensure_exclusive(ctx):
@module.cmd(
name="rooms",
desc="Schedule an accountability study session.",
group="Productivity"
name="schedule",
desc="View your schedule, and get rewarded for attending scheduled sessions!",
group="Productivity",
aliases=('rooms', 'sessions')
)
@in_guild()
async def cmd_rooms(ctx):
"""
Usage``:
{prefix}rooms
{prefix}rooms book
{prefix}rooms cancel
{prefix}schedule
{prefix}schedule book
{prefix}schedule cancel
Description:
View your accountability profile with `{prefix}rooms`.
Use `{prefix}rooms book` to book an accountability session!
Use `{prefix}rooms cancel` to cancel a booked session.
View your schedule with `{prefix}schedule`.
Use `{prefix}schedule book` to schedule a session at a selected time..
Use `{prefix}schedule cancel` to cancel a scheduled session.
"""
lower = ctx.args.lower()
splits = lower.split()
command = splits[0] if splits else None
if not ctx.guild_settings.accountability_category.value:
return await ctx.error_reply("The accountability system isn't set up!")
return await ctx.error_reply("The scheduled session system isn't set up!")
# First grab the sessions the member is booked in
joined_rows = accountability_member_info.select_where(
@@ -94,7 +95,7 @@ async def cmd_rooms(ctx):
if command == 'cancel':
if not joined_rows:
return await ctx.error_reply("You have no bookings to cancel!")
return await ctx.error_reply("You have no scheduled sessions to cancel!")
# Show unbooking menu
lines = [
@@ -102,9 +103,9 @@ async def cmd_rooms(ctx):
for i, row in enumerate(joined_rows)
]
out_msg = await ctx.reply(
content="Please reply with the number(s) of the rooms you want to cancel. E.g. `1, 3, 5` or `1-3, 7-8`.",
content="Please reply with the number(s) of the sessions you want to cancel. E.g. `1, 3, 5` or `1-3, 7-8`.",
embed=discord.Embed(
title="Please choose the bookings you want to cancel.",
title="Please choose the sessions you want to cancel.",
description='\n'.join(lines),
colour=discord.Colour.orange()
).set_footer(
@@ -116,7 +117,7 @@ async def cmd_rooms(ctx):
await ctx.cancellable(
out_msg,
cancel_message="Cancel menu closed, no accountability sessions were cancelled.",
cancel_message="Cancel menu closed, no scheduled sessions were cancelled.",
timeout=70
)
@@ -133,7 +134,7 @@ async def cmd_rooms(ctx):
await out_msg.edit(
content=None,
embed=discord.Embed(
description="Cancel menu timed out, no accountability sessions were cancelled.",
description="Cancel menu timed out, no scheduled sessions were cancelled.",
colour=discord.Colour.red()
)
)
@@ -156,7 +157,7 @@ async def cmd_rooms(ctx):
for index in parse_ranges(message.content) if index < len(joined_rows)
]
if not to_cancel:
return await ctx.error_reply("No valid bookings selected for cancellation.")
return await ctx.error_reply("No valid sessions selected for cancellation.")
elif any(row['start_at'] < utc_now() for row in to_cancel):
return await ctx.error_reply("You can't cancel a running session!")
@@ -189,7 +190,7 @@ async def cmd_rooms(ctx):
remaining = [row for row in joined_rows if row['slotid'] not in slotids]
if not remaining:
await ctx.embed_reply("Cancelled all your upcoming accountability sessions!")
await ctx.embed_reply("Cancelled all your upcoming scheduled sessions!")
else:
next_booked_time = min(row['start_at'] for row in remaining)
if len(to_cancel) > 1:
@@ -245,9 +246,11 @@ async def cmd_rooms(ctx):
# TODO: Nicer embed
# TODO: Don't allow multi bookings if the member has a bad attendance rate
out_msg = await ctx.reply(
content="Please reply with the number(s) of the rooms you want to join. E.g. `1, 3, 5` or `1-3, 7-8`.",
content=(
"Please reply with the number(s) of the sessions you want to book. E.g. `1, 3, 5` or `1-3, 7-8`."
),
embed=discord.Embed(
title="Please choose the sessions you want to book.",
title="Please choose the sessions you want to schedule.",
description='\n'.join(lines),
colour=discord.Colour.orange()
).set_footer(
@@ -354,10 +357,10 @@ async def cmd_rooms(ctx):
# Ack purchase
embed = discord.Embed(
title="You have booked the following session{}!".format('s' if len(to_book) > 1 else ''),
title="You have scheduled the following session{}!".format('s' if len(to_book) > 1 else ''),
description=(
"*Please attend all your booked sessions!*\n"
"*If you can't attend, cancel with* `{}rooms cancel`\n\n{}"
"*Please attend all your scheduled sessions!*\n"
"*If you can't attend, cancel with* `{}schedule cancel`\n\n{}"
).format(
ctx.best_prefix,
'\n'.join(time_format(time) for time in to_book),
@@ -365,7 +368,7 @@ async def cmd_rooms(ctx):
colour=discord.Colour.orange()
).set_footer(
text=(
"Use {prefix}rooms to see your current bookings.\n"
"Use {prefix}schedule to see your current schedule.\n"
).format(prefix=ctx.best_prefix)
)
try:
@@ -400,10 +403,10 @@ async def cmd_rooms(ctx):
if not (history or joined_rows):
# First-timer information
about = (
"You haven't joined any accountability sessions yet!\n"
"Book a session by typing **`{}rooms book`** and selecting "
"You haven't scheduled any study sessions yet!\n"
"Schedule a session by typing **`{}schedule book`** and selecting "
"the hours you intend to study, "
"then attend by joining the accountability voice channel when the session starts!\n"
"then attend by joining the session voice channel when it starts!\n"
"Only if everyone attends will they get the bonus of `{}` LionCoins!\n"
"Let's all do our best and keep each other accountable 🔥"
).format(
@@ -492,7 +495,7 @@ async def cmd_rooms(ctx):
total_count,
(attended_count * 100) / total_count,
),
"Time": "**{:02}:{:02}** spent in accountability rooms.".format(
"Time": "**{:02}:{:02}** in scheduled sessions.".format(
total_duration // 3600,
(total_duration % 3600) // 60
),
@@ -525,11 +528,24 @@ async def cmd_rooms(ctx):
),
_extra="GROUP BY start_at, slotid, guildid ORDER BY start_at ASC"
)
attendees = {row['start_at']: (row['num'], client.get_guild(row['guildid'])) for row in rows}
attendees = {
row['start_at']: (row['num'], row['guildid']) for row in rows
}
attendee_pad = max((len(str(num)) for num, _ in attendees.values()), default=1)
# TODO: Allow cancel to accept multiselect keys as args
show_guild = any(guild != ctx.guild for _, guild in attendees.values())
show_guild = any(guildid != ctx.guild.id for _, guildid in attendees.values())
guild_map = {}
if show_guild:
for _, guildid in attendees.values():
if guildid not in guild_map:
guild = ctx.client.get_guild(guildid)
if not guild:
try:
guild = await ctx.client.fetch_guild(guildid)
except discord.HTTPException:
guild = None
guild_map[guildid] = guild
booked_list = '\n'.join(
"`{:>{}}` attendees | {} {}".format(
@@ -537,24 +553,24 @@ async def cmd_rooms(ctx):
attendee_pad,
time_format(start),
"" if not show_guild else (
"on this server" if guild == ctx.guild else "in **{}**".format(
guild.name if guild else guild.id
"on this server" if guildid == ctx.guild.id else "in **{}**".format(
guild_map[guildid] or "Unknown"
)
)
) for start, (num, guild) in attendees.items()
) for start, (num, guildid) in attendees.items()
)
booked_field = (
"{}\n\n"
"*If you can't make your booking, please cancel using `{}rooms cancel`!*"
"*If you can't make your session, please cancel using `{}schedule cancel`!*"
).format(booked_list, ctx.best_prefix)
# Temporary footer for acclimatisation
# footer = "All times are displayed in your own timezone!"
footer = "Book another session using {}rooms book".format(ctx.best_prefix)
footer = "Book another session using {}schedule book".format(ctx.best_prefix)
else:
booked_field = (
"Your schedule is empty!\n"
"Book another session using `{}rooms book`."
"Book another session using `{}schedule book`."
).format(ctx.best_prefix)
footer = "Please keep your DMs open for notifications!"
@@ -563,7 +579,7 @@ async def cmd_rooms(ctx):
colour=discord.Colour.orange(),
description=desc,
).set_author(
name="Accountability profile for {}".format(ctx.author.name),
name="Schedule statistics for {}".format(ctx.author.name),
icon_url=ctx.author.avatar_url
).set_footer(
text=footer,

View File

@@ -94,9 +94,9 @@ async def open_next(start_time):
if not slot.category:
# Log and unload guild
aguild.guild_settings.event_log.log(
"The accountability category couldn't be found!\n"
"Shutting down the accountability system in this server.\n"
"To re-activate, please reconfigure `config accountability_category`."
"The scheduled session category couldn't be found!\n"
"Shutting down the scheduled session system in this server.\n"
"To re-activate, please reconfigure `config session_category`."
)
AccountabilityGuild.cache.pop(aguild.guildid, None)
await slot.cancel()
@@ -106,16 +106,16 @@ async def open_next(start_time):
# Create a new lobby
try:
channel = await guild.create_text_channel(
name="accountability-lobby",
name="session-lobby",
category=slot.category,
reason="Automatic creation of accountability lobby."
reason="Automatic creation of scheduled session lobby."
)
aguild.guild_settings.accountability_lobby.value = channel
slot.lobby = channel
except discord.HTTPException:
# Event log failure and skip session
aguild.guild_settings.event_log.log(
"Failed to create the accountability lobby text channel.\n"
"Failed to create the scheduled session lobby text channel.\n"
"Please set the lobby channel manually with `config`."
)
await slot.cancel()
@@ -123,7 +123,7 @@ async def open_next(start_time):
# Event log creation
aguild.guild_settings.event_log.log(
"Automatically created an accountability lobby channel {}.".format(channel.mention)
"Automatically created a scheduled session lobby channel {}.".format(channel.mention)
)
results = await slot.open()
@@ -221,7 +221,7 @@ async def turnover():
movement_tasks = (
mem.member.edit(
voice_channel=slot.channel,
reason="Moving to booked accountability session."
reason="Moving to scheduled session."
)
for slot in current_slots
for mem in slot.members.values()
@@ -317,7 +317,7 @@ async def _accountability_loop():
except Exception:
# Unknown exception. Catch it so the loop doesn't die.
client.log(
"Error while opening new accountability rooms! "
"Error while opening new scheduled sessions! "
"Exception traceback follows.\n{}".format(
traceback.format_exc()
),
@@ -332,7 +332,7 @@ async def _accountability_loop():
except Exception:
# Unknown exception. Catch it so the loop doesn't die.
client.log(
"Error while starting accountability rooms! "
"Error while starting scheduled sessions! "
"Exception traceback follows.\n{}".format(
traceback.format_exc()
),

View File

@@ -60,7 +60,7 @@ async def cmd_send(ctx):
return await ctx.embed_reply("We are still waiting for {} to open an account.".format(target.mention))
# Finally, send the amount and the ack message
target_lion.addCoins(amount)
target_lion.addCoins(amount, ignorebonus=True)
source_lion.addCoins(-amount)
embed = discord.Embed(

View File

@@ -61,10 +61,10 @@ async def cmd_set(ctx):
# Postgres `coins` column is `integer`, sanity check postgres int limits - which are smalled than python int range
target_coins_to_set = target_lion.coins + amount
if target_coins_to_set >= 0 and target_coins_to_set <= POSTGRES_INT_MAX:
target_lion.addCoins(amount)
target_lion.addCoins(amount, ignorebonus=True)
elif target_coins_to_set < 0:
target_coins_to_set = -target_lion.coins # Coins cannot go -ve, cap to 0
target_lion.addCoins(target_coins_to_set)
target_lion.addCoins(target_coins_to_set, ignorebonus=True)
target_coins_to_set = 0
else:
return await ctx.embed_reply("Member coins cannot be more than {}".format(POSTGRES_INT_MAX))

View File

@@ -16,7 +16,7 @@ cat_pages = {
'Administration': ('Meta', 'Guild Roles', 'New Members'),
'Moderation': ('Moderation', 'Video Channels'),
'Productivity': ('Study Tracking', 'TODO List', 'Workout'),
'Study Rooms': ('Rented Rooms', 'Accountability Rooms'),
'Study Rooms': ('Rented Rooms', 'Scheduled Sessions'),
}
# Descriptions of each configuration category

View File

@@ -25,12 +25,12 @@ class greeting_channel(stypes.Channel, GuildSetting):
attr_name = 'greeting_channel'
_data_column = 'greeting_channel'
display_name = "greeting_channel"
desc = "Channel to send the greeting message in"
display_name = "welcome_channel"
desc = "Channel to send the welcome message in"
long_desc = (
"Channel to post the `greeting_message` in when a new user joins the server. "
"Accepts `DM` to indicate the greeting should be direct messaged to the new member."
"Channel to post the `welcome_message` in when a new user joins the server. "
"Accepts `DM` to indicate the welcome should be sent via direct message."
)
_accepts = (
"Text Channel name/id/mention, or `DM`, or `None` to disable."
@@ -78,11 +78,11 @@ class greeting_channel(stypes.Channel, GuildSetting):
def success_response(self):
value = self.value
if not value:
return "Greeting messages are disabled."
return "Welcome messages are disabled."
elif value == self.DMCHANNEL:
return "Greeting messages will be sent via direct message."
return "Welcome messages will be sent via direct message."
else:
return "Greeting messages will be posted in {}".format(self.formatted)
return "Welcome messages will be posted in {}".format(self.formatted)
@GuildSettings.attach_setting
@@ -92,11 +92,11 @@ class greeting_message(stypes.Message, GuildSetting):
attr_name = 'greeting_message'
_data_column = 'greeting_message'
display_name = 'greeting_message'
desc = "Greeting message sent to welcome new members."
display_name = 'welcome_message'
desc = "Welcome message sent to welcome new members."
long_desc = (
"Message to send to the configured `greeting_channel` when a member joins the server for the first time."
"Message to send to the configured `welcome_channel` when a member joins the server for the first time."
)
_default = r"""
@@ -133,7 +133,7 @@ class greeting_message(stypes.Message, GuildSetting):
@property
def success_response(self):
return "The greeting message has been set!"
return "The welcome message has been set!"
@GuildSettings.attach_setting
@@ -144,10 +144,10 @@ class returning_message(stypes.Message, GuildSetting):
_data_column = 'returning_message'
display_name = 'returning_message'
desc = "Greeting message sent to returning members."
desc = "Welcome message sent to returning members."
long_desc = (
"Message to send to the configured `greeting_channel` when a member returns to the server."
"Message to send to the configured `welcome_channel` when a member returns to the server."
)
_default = r"""

View File

@@ -1,4 +1,5 @@
import asyncio
from codecs import ignore_errors
import logging
import traceback
import datetime
@@ -500,7 +501,7 @@ class ReactionRoleMessage:
if price and refund:
# Give the user the refund
lion = Lion.fetch(self.guild.id, member.id)
lion.addCoins(price)
lion.addCoins(price, ignorebonus=True)
# Notify the user
embed = discord.Embed(

View File

@@ -21,7 +21,7 @@ group_hints = {
}
standard_group_order = (
('🆕 Pomodoro', 'Productivity', 'Statistics', 'Economy', 'Personal Settings', 'Meta')
('🆕 Pomodoro', 'Productivity', 'Statistics', 'Economy', 'Personal Settings', 'Meta'),
)
mod_group_order = (

View File

@@ -13,6 +13,7 @@ ticket_info = RowTable(
'expiry',
'pardoned_by', 'pardoned_at', 'pardoned_reason'),
'ticketid',
cache_size=20000
)
tickets = Table('tickets')

View File

@@ -107,7 +107,7 @@ class video_grace_period(settings.Duration, GuildSetting):
"before they will be kicked from the channel, and warned or studybanned (if enabled)."
)
_default = 45
_default = 90
_default_multiplier = 1
@classmethod

View File

@@ -1,8 +1,8 @@
from data import RowTable
from data.interfaces import RowTable
reminders = RowTable(
'reminders',
('reminderid', 'userid', 'remind_at', 'content', 'message_link', 'interval', 'created_at'),
('reminderid', 'userid', 'remind_at', 'content', 'message_link', 'interval', 'created_at', 'title', 'footer'),
'reminderid'
)

View File

@@ -150,12 +150,14 @@ class Reminder:
# Build the message embed
embed = discord.Embed(
title="You asked me to remind you!",
title="You asked me to remind you!" if self.data.title is None else self.data.title,
colour=discord.Colour.orange(),
description=self.data.content,
timestamp=datetime.datetime.utcnow()
)
embed.add_field(name="Context?", value="[Click here]({})".format(self.data.message_link))
if self.data.message_link:
embed.add_field(name="Context?", value="[Click here]({})".format(self.data.message_link))
if self.data.interval:
embed.add_field(
@@ -165,6 +167,9 @@ class Reminder:
)
)
if self.data.footer:
embed.set_footer(text=self.data.footer)
# Update the reminder data, and reschedule if required
if self.data.interval:
next_time = self.data.remind_at + datetime.timedelta(seconds=self.data.interval)

View File

@@ -42,7 +42,7 @@ class rent_member_limit(settings.Integer, GuildSetting):
display_name = "rent_member_limit"
desc = "Maximum number of people that can be added to a rented room."
_default = 10
_default = 24
long_desc = (
"Maximum number of people a member can add to a rented private voice channel."

View File

@@ -98,6 +98,8 @@ async def cmd_setprofile(ctx, flags):
return await ctx.error_reply(
f"Sorry, you can have a maximum of `{MAX_TAGS}` tags!"
)
if tagid == 0:
return await ctx.error_reply("Tags start at `1`!")
# Retrieve the user's current taglist
rows = profile_tags.select_where(

View File

@@ -327,7 +327,7 @@ class Timer:
Remove the timer.
"""
# Remove timer from cache
self.timers.pop(self.channelid)
self.timers.pop(self.channelid, None)
# Cancel the loop
if self._run_task:
@@ -372,6 +372,11 @@ class Timer:
except asyncio.CancelledError:
break
# Destroy the timer if our voice channel no longer exists
if not self.channel:
await self.destroy()
break
if self._state.end < utc_now():
asyncio.create_task(self.notify_change_stage(self._state, self.current_stage))
else:

View File

@@ -61,7 +61,7 @@ class Session:
raise ValueError("A session for this member already exists!")
# If the user is study capped, schedule the session start for the next day
if (lion := Lion.fetch(guildid, userid)).remaining_study_today <= 0:
if (lion := Lion.fetch(guildid, userid)).remaining_study_today <= 10:
if pending := cls.members_pending[guildid].pop(userid, None):
pending.cancel()
task = asyncio.create_task(cls._delayed_start(guildid, userid, member, state))
@@ -181,12 +181,13 @@ class Session:
self._expiry_task.cancel()
# Wait for the maximum session length
self._expiry_task = asyncio.create_task(asyncio.sleep(self.lion.remaining_study_today))
try:
self._expiry_task = await asyncio.sleep(self.lion.remaining_study_today)
await self._expiry_task
except asyncio.CancelledError:
pass
else:
if self.lion.remaining_study_today <= 0:
if self.lion.remaining_study_today <= 10:
# End the session
# Note that the user will not automatically start a new session when the day starts
# TODO: Notify user? Disconnect them?
@@ -253,7 +254,7 @@ async def session_voice_tracker(client, member, before, after):
return
guild = member.guild
Lion.fetch(guild.id, member.id)
Lion.fetch(guild.id, member.id).update_saved_data(member)
session = Session.get(guild.id, member.id)
if before.channel == after.channel:

View File

@@ -16,7 +16,7 @@ class task_limit(settings.Integer, GuildSetting):
display_name = "task_limit"
desc = "Maximum number of tasks each user may have."
_default = 30
_default = 99
long_desc = (
"Maximum number of tasks each user may have in the todo system."

View File

@@ -0,0 +1,6 @@
from .module import module
from . import webhook
from . import commands
from . import data
from . import settings

View File

@@ -0,0 +1,78 @@
import discord
from .module import module
from bot.cmdClient.checks import in_guild, is_owner
from settings.user_settings import UserSettings
from LionContext import LionContext
from .webhook import on_dbl_vote
from .utils import lion_loveemote
@module.cmd(
"forcevote",
desc="Simulate a Topgg Vote from the given user.",
group="Bot Admin",
)
@is_owner()
async def cmd_forcevote(ctx: LionContext):
"""
Usage``:
{prefix}forcevote
Description:
Simulate Top.gg vote without actually a confirmation from Topgg site.
Can be used for force a vote for testing or if topgg has an error or production time bot error.
"""
target = ctx.author
# Identify the target
if ctx.args:
if not ctx.msg.mentions:
return await ctx.error_reply("Please mention a user to simulate a vote!")
target = ctx.msg.mentions[0]
await on_dbl_vote({"user": target.id, "type": "test"})
return await ctx.reply('Topgg vote simulation successful on {}'.format(target), suggest_vote=False)
@module.cmd(
"vote",
desc="[Vote](https://top.gg/bot/889078613817831495/vote) for me to get 25% more LCs!",
group="Economy",
aliases=('topgg', 'topggvote', 'upvote')
)
@in_guild()
async def cmd_vote(ctx: LionContext):
"""
Usage``:
{prefix}vote
Description:
Get Top.gg bot's link for +25% Economy boost.
"""
embed = discord.Embed(
title="Claim your boost!",
description=(
"Please click [here](https://top.gg/bot/889078613817831495/vote) to vote and support our bot!\n\n"
"Thank you! {}.".format(lion_loveemote)
),
colour=discord.Colour.orange()
).set_thumbnail(
url="https://cdn.discordapp.com/attachments/908283085999706153/933012309532614666/lion-love.png"
)
return await ctx.reply(embed=embed, suggest_vote=False)
@module.cmd(
"vote_reminder",
group="Personal Settings",
desc="Turn on/off boost reminders."
)
async def cmd_remind_vote(ctx: LionContext):
"""
Usage:
`{prefix}vote_reminder on`
`{prefix}vote_reminder off`
Enable or disable DM boost reminders.
"""
await UserSettings.settings.vote_remainder.command(ctx, ctx.author.id)

View File

@@ -0,0 +1,8 @@
from data.interfaces import RowTable
topggvotes = RowTable(
'topgg',
('voteid', 'userid', 'boostedTimestamp'),
'voteid'
)

View File

@@ -0,0 +1,70 @@
from LionModule import LionModule
from LionContext import LionContext
from core.lion import Lion
from .utils import get_last_voted_timestamp, lion_loveemote, lion_yayemote
from .webhook import init_webhook
module = LionModule("Topgg")
upvote_info = "You have a boost available {}, to support our project and earn **25% more LionCoins** type `{}vote` {}"
@module.launch_task
async def attach_topgg_webhook(client):
if client.shard_id == 0:
init_webhook()
client.log("Attached top.gg voiting webhook.", context="TOPGG")
@module.launch_task
async def register_hook(client):
LionContext.reply.add_wrapper(topgg_reply_wrapper)
Lion.register_economy_bonus(economy_bonus)
client.log("Loaded top.gg hooks.", context="TOPGG")
@module.unload_task
async def unregister_hook(client):
Lion.unregister_economy_bonus(economy_bonus)
LionContext.reply.remove_wrapper(topgg_reply_wrapper.__name__)
client.log("Unloaded top.gg hooks.", context="TOPGG")
async def topgg_reply_wrapper(func, *args, suggest_vote=True, **kwargs):
ctx = args[0]
if not suggest_vote:
pass
elif ctx.cmd and ctx.cmd.name == 'config':
pass
elif ctx.cmd and ctx.cmd.name == 'help' and ctx.args and ctx.args.split(maxsplit=1)[0].lower() == 'vote':
pass
elif not get_last_voted_timestamp(args[0].author.id):
upvote_info_formatted = upvote_info.format(lion_yayemote, args[0].best_prefix, lion_loveemote)
if 'embed' in kwargs:
# Add message as an extra embed field
kwargs['embed'].add_field(
name="\u200b",
value=(
upvote_info_formatted
),
inline=False
)
else:
# Add message to content
if 'content' in kwargs and kwargs['content'] and len(kwargs['content']) + len(upvote_info_formatted) < 1998:
kwargs['content'] += '\n\n' + upvote_info_formatted
elif len(args) > 1 and len(args[1]) + len(upvote_info_formatted) < 1998:
args = list(args)
args[1] += '\n\n' + upvote_info_formatted
else:
kwargs['content'] = upvote_info_formatted
return await func(*args, **kwargs)
def economy_bonus(lion):
return 1.25 if get_last_voted_timestamp(lion.userid) else 1

View File

@@ -0,0 +1,50 @@
from settings.user_settings import UserSettings, UserSetting
from settings.setting_types import Boolean
from modules.reminders.reminder import Reminder
from modules.reminders.data import reminders
from .utils import create_remainder, remainder_content, topgg_upvote_link
@UserSettings.attach_setting
class topgg_vote_remainder(Boolean, UserSetting):
attr_name = 'vote_remainder'
_data_column = 'topgg_vote_reminder'
_default = True
display_name = 'vote_reminder'
desc = r"Toggle automatic reminders to support me for a 25% LionCoin boost."
long_desc = (
"Did you know that you can [vote for me]({vote_link}) to help me help other people reach their goals? "
"And you get a **25% boost** to all LionCoin income you make across all servers!\n"
"Enable this setting if you want me to let you know when you can vote again!"
).format(vote_link=topgg_upvote_link)
@property
def success_response(self):
if self.value:
# Check if reminder is already running
create_remainder(self.id)
return (
"Thank you for supporting me! I will remind in your DMs when you can vote next! "
"(Please make sure your DMs are open, otherwise I can't reach you!)"
)
else:
# Check if reminder is already running and get its id
r = reminders.select_one_where(
userid=self.id,
select_columns='reminderid',
content=remainder_content,
_extra="ORDER BY remind_at DESC LIMIT 1"
)
# Cancel and delete Remainder if already running
if r:
Reminder.delete(r['reminderid'])
return (
"I will no longer send you voting reminders."
)

View File

@@ -0,0 +1,97 @@
import discord
import datetime
from meta import sharding
from meta import conf
from meta.client import client
from utils.lib import utc_now
from settings.setting_types import Integer
from modules.reminders.reminder import Reminder
from modules.reminders.data import reminders
from . import data as db
from data.conditions import GEQ
topgg_upvote_link = 'https://top.gg/bot/889078613817831495/vote'
remainder_content = (
"You can now vote again on top.gg!\n"
"Click [here]({}) to vote, thank you for the support!"
).format(topgg_upvote_link)
lion_loveemote = conf.emojis.getemoji('lionlove')
lion_yayemote = conf.emojis.getemoji('lionyay')
def get_last_voted_timestamp(userid: Integer):
"""
Will return None if user has not voted in [-12.5hrs till now]
else will return a Tuple containing timestamp of when exactly she voted
"""
return db.topggvotes.select_one_where(
userid=userid,
select_columns="boostedTimestamp",
boostedTimestamp=GEQ(utc_now() - datetime.timedelta(hours=12.5)),
_extra="ORDER BY boostedTimestamp DESC LIMIT 1"
)
def create_remainder(userid):
"""
Checks if a remainder is already running (immaterial of remind_at time)
If no remainder exists creates a new remainder and schedules it
"""
if not reminders.select_one_where(
userid=userid,
content=remainder_content,
_extra="ORDER BY remind_at DESC LIMIT 1"
):
last_vote_time = get_last_voted_timestamp(userid)
# if no, Create reminder
reminder = Reminder.create(
userid=userid,
# TODO using content as a selector is not a good method
content=remainder_content,
message_link=None,
interval=None,
title="Your boost is now available! {}".format(lion_yayemote),
footer="Use `{}vote_reminder off` to stop receiving reminders.".format(client.prefix),
remind_at=(
last_vote_time[0] + datetime.timedelta(hours=12.5)
if last_vote_time else
utc_now() + datetime.timedelta(minutes=5)
)
# remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2)
)
# Schedule reminder
if sharding.shard_number == 0:
reminder.schedule()
async def send_user_dm(userid):
# Send the message, if possible
if not (user := client.get_user(userid)):
try:
user = await client.fetch_user(userid)
except discord.HTTPException:
pass
if user:
try:
embed = discord.Embed(
title="Thank you for supporting our bot on Top.gg! {}".format(lion_yayemote),
description=(
"By voting every 12 hours you will allow us to reach and help "
"even more students all over the world.\n"
"Thank you for supporting us, enjoy your LionCoins boost!"
),
colour=discord.Colour.orange()
).set_image(
url="https://cdn.discordapp.com/attachments/908283085999706153/932737228440993822/lion-yay.png"
)
await user.send(embed=embed)
except discord.HTTPException:
# Nothing we can really do here. Maybe tell the user about their reminder next time?
pass

View File

@@ -0,0 +1,40 @@
from meta.client import client
from settings.user_settings import UserSettings
from utils.lib import utc_now
from meta.config import conf
import topgg
from .utils import db, send_user_dm, create_remainder
@client.event
async def on_dbl_vote(data):
"""An event that is called whenever someone votes for the bot on Top.gg."""
client.log(f"Received a vote: \n{data}", context='Topgg')
db.topggvotes.insert(
userid=data['user'],
boostedTimestamp=utc_now()
)
await send_user_dm(data['user'])
if UserSettings.settings.vote_remainder.value:
create_remainder(data['user'])
if data["type"] == "test":
return client.dispatch("dbl_test", data)
@client.event
async def on_dbl_test(data):
"""An event that is called whenever someone tests the webhook system for your bot on Top.gg."""
client.log(f"Received a test vote:\n{data}", context='Topgg')
def init_webhook():
client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook(
conf.bot.get("topgg_route"),
conf.bot.get("topgg_password")
)
client.topgg_webhook.run(conf.bot.get("topgg_port"))

View File

@@ -130,8 +130,8 @@ class Integer(SettingType):
accepts = "An integer."
# Set limits on the possible integers
_min = -4096
_max = 4096
_min = -2147483647
_max = 2147483647
@classmethod
def _data_from_value(cls, id: int, value: Optional[bool], **kwargs):

View File

@@ -1,6 +1,6 @@
import asyncio
import discord
from cmdClient import Context
from LionContext import LionContext as Context
from cmdClient.lib import SafeCancellation
from data import tables

View File

@@ -1,6 +1,6 @@
import asyncio
import discord
from cmdClient import Context
from LionContext import LionContext as Context
from cmdClient.lib import UserCancelled, ResponseTimedOut
from .lib import paginate_list

View File

@@ -1,9 +1,9 @@
import asyncio
import discord
from cmdClient import Context
from LionContext import LionContext as Context
from cmdClient.lib import InvalidContext, UserCancelled, ResponseTimedOut, SafeCancellation
from . import interactive
from . import interactive as _interactive
@Context.util

View File

@@ -14,3 +14,11 @@ data_appid = LionBot
shard_count = 1
lion_sync_period = 60
topgg_password =
topgg_route =
topgg_port =
[EMOJIS]
lionyay =
lionlove =

View File

@@ -0,0 +1,87 @@
ALTER TABLE user_config
ADD COLUMN topgg_vote_reminder BOOLEAN;
ALTER TABLE reminders
ADD COLUMN title TEXT,
ADD COLUMN footer TEXT;
-- Topgg Data {{{
CREATE TABLE IF NOT EXISTS topgg(
voteid SERIAL PRIMARY KEY,
userid BIGINT NOT NULL,
boostedTimestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp);
-- }}}
DROP FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT);
CREATE FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT)
RETURNS SETOF members
AS $$
BEGIN
RETURN QUERY
WITH
current_sesh AS (
DELETE FROM current_sessions
WHERE guildid=_guildid AND userid=_userid
RETURNING
*,
EXTRACT(EPOCH FROM (NOW() - start_time)) AS total_duration,
stream_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - stream_start)), 0) AS total_stream_duration,
video_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - video_start)), 0) AS total_video_duration,
live_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - live_start)), 0) AS total_live_duration
), bonus_userid AS (
SELECT COUNT(boostedTimestamp),
CASE WHEN EXISTS (
SELECT 1 FROM Topgg
WHERE Topgg.userid=_userid AND EXTRACT(EPOCH FROM (NOW() - boostedTimestamp)) < 12.5*60*60
) THEN
(array_agg(
CASE WHEN boostedTimestamp <= current_sesh.start_time THEN
1.25
ELSE
(((current_sesh.total_duration - EXTRACT(EPOCH FROM (boostedTimestamp - current_sesh.start_time)))/current_sesh.total_duration)*0.25)+1
END))[1]
ELSE
1
END
AS bonus
FROM Topgg, current_sesh
WHERE Topgg.userid=_userid AND EXTRACT(EPOCH FROM (NOW() - boostedTimestamp)) < 12.5*60*60
ORDER BY (array_agg(boostedTimestamp))[1] DESC LIMIT 1
), saved_sesh AS (
INSERT INTO session_history (
guildid, userid, channelid, rating, tag, channel_type, start_time,
duration, stream_duration, video_duration, live_duration,
coins_earned
) SELECT
guildid, userid, channelid, rating, tag, channel_type, start_time,
total_duration, total_stream_duration, total_video_duration, total_live_duration,
((total_duration * hourly_coins + live_duration * hourly_live_coins) * bonus_userid.bonus )/ 3600
FROM current_sesh, bonus_userid
RETURNING *
)
UPDATE members
SET
tracked_time=(tracked_time + saved_sesh.duration),
coins=(coins + saved_sesh.coins_earned)
FROM saved_sesh
WHERE members.guildid=saved_sesh.guildid AND members.userid=saved_sesh.userid
RETURNING members.*;
END;
$$ LANGUAGE PLPGSQL;
-- }}}
ALTER TABLE user_config
ADD COLUMN avatar_hash TEXT;
ALTER TABLE guild_config
ADD COLUMN name TEXT;
ALTER TABLE members
ADD COLUMN display_name TEXT;
INSERT INTO VersionHistory (version, author) VALUES (9, 'v8-v9 migration');

View File

@@ -4,7 +4,7 @@ CREATE TABLE VersionHistory(
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
author TEXT
);
INSERT INTO VersionHistory (version, author) VALUES (8, 'Initial Creation');
INSERT INTO VersionHistory (version, author) VALUES (9, 'Initial Creation');
CREATE OR REPLACE FUNCTION update_timestamp_column()
@@ -41,7 +41,9 @@ CREATE TABLE global_guild_blacklist(
-- User configuration data {{{
CREATE TABLE user_config(
userid BIGINT PRIMARY KEY,
timezone TEXT
timezone TEXT,
topgg_vote_reminder,
avatar_hash TEXT
);
-- }}}
@@ -79,7 +81,8 @@ CREATE TABLE guild_config(
starting_funds INTEGER,
persist_roles BOOLEAN,
daily_study_cap INTEGER,
pomodoro_channel BIGINT
pomodoro_channel BIGINT,
name TEXT
);
CREATE TABLE ignored_members(
@@ -166,7 +169,9 @@ CREATE TABLE reminders(
content TEXT NOT NULL,
message_link TEXT,
interval INTEGER,
created_at TIMESTAMP DEFAULT (now() at time zone 'utc')
created_at TIMESTAMP DEFAULT (now() at time zone 'utc'),
title TEXT,
footer TEXT
);
CREATE INDEX reminder_users ON reminders (userid);
-- }}}
@@ -402,6 +407,7 @@ CREATE TABLE members(
last_workout_start TIMESTAMP,
last_study_badgeid INTEGER REFERENCES study_badges ON DELETE SET NULL,
video_warned BOOLEAN DEFAULT FALSE,
display_name TEXT,
_timestamp TIMESTAMP DEFAULT (now() at time zone 'utc'),
PRIMARY KEY(guildid, userid)
);
@@ -417,7 +423,7 @@ CREATE TYPE SessionChannelType AS ENUM (
'STANDARD',
'ACCOUNTABILITY',
'RENTED',
'EXTERNAL',
'EXTERNAL'
);
@@ -512,6 +518,25 @@ AS $$
stream_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - stream_start)), 0) AS total_stream_duration,
video_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - video_start)), 0) AS total_video_duration,
live_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - live_start)), 0) AS total_live_duration
), bonus_userid AS (
SELECT COUNT(boostedTimestamp),
CASE WHEN EXISTS (
SELECT 1 FROM Topgg
WHERE Topgg.userid=_userid AND EXTRACT(EPOCH FROM (NOW() - boostedTimestamp)) < 12.5*60*60
) THEN
(array_agg(
CASE WHEN boostedTimestamp <= current_sesh.start_time THEN
1.25
ELSE
(((current_sesh.total_duration - EXTRACT(EPOCH FROM (boostedTimestamp - current_sesh.start_time)))/current_sesh.total_duration)*0.25)+1
END))[1]
ELSE
1
END
AS bonus
FROM Topgg, current_sesh
WHERE Topgg.userid=_userid AND EXTRACT(EPOCH FROM (NOW() - boostedTimestamp)) < 12.5*60*60
ORDER BY (array_agg(boostedTimestamp))[1] DESC LIMIT 1
), saved_sesh AS (
INSERT INTO session_history (
guildid, userid, channelid, rating, tag, channel_type, start_time,
@@ -520,8 +545,8 @@ AS $$
) SELECT
guildid, userid, channelid, rating, tag, channel_type, start_time,
total_duration, total_stream_duration, total_video_duration, total_live_duration,
(total_duration * hourly_coins + live_duration * hourly_live_coins) / 3600
FROM current_sesh
((total_duration * hourly_coins + live_duration * hourly_live_coins) * bonus_userid.bonus )/ 3600
FROM current_sesh, bonus_userid
RETURNING *
)
UPDATE members
@@ -766,4 +791,13 @@ create TABLE timers(
CREATE INDEX timers_guilds ON timers (guildid);
-- }}}
-- Topgg Data {{{
create TABLE topgg(
voteid SERIAL PRIMARY KEY,
userid BIGINT NOT NULL,
boostedTimestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp);
-- }}}
-- vim: set fdm=marker:

View File

@@ -6,3 +6,4 @@ discord.py==1.7.3
iso8601==0.1.16
psycopg2==2.9.1
pytz==2021.1
topggpy