From 7a8342c7f700a50dd7c73fbb8bac7e7e4e05374f Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Wed, 12 Jan 2022 19:58:27 +0530 Subject: [PATCH 01/49] Fix Database Schema Extra ',' resulting in errors when freshly reconstructing lionbot DB --- data/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/schema.sql b/data/schema.sql index 627525fe..51432086 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -417,7 +417,7 @@ CREATE TYPE SessionChannelType AS ENUM ( 'STANDARD', 'ACCOUNTABILITY', 'RENTED', - 'EXTERNAL', + 'EXTERNAL' ); From 4f5bb77a281fb8b76bace263a5e156201ab5c62b Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 13 Jan 2022 06:43:52 +0200 Subject: [PATCH 02/49] fix (help): Add missing comma. --- bot/modules/meta/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/modules/meta/help.py b/bot/modules/meta/help.py index 65209233..ada81e18 100644 --- a/bot/modules/meta/help.py +++ b/bot/modules/meta/help.py @@ -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 = ( From 3e0c1be33e2e6200eeedabac5676dc3803f0bfb7 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Thu, 13 Jan 2022 14:03:23 +0530 Subject: [PATCH 03/49] Init Framework --- bot/modules/__init__.py | 1 + bot/modules/topgg/__init__.py | 3 +++ bot/modules/topgg/module.py | 3 +++ bot/modules/topgg/topgg.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 bot/modules/topgg/__init__.py create mode 100644 bot/modules/topgg/module.py create mode 100644 bot/modules/topgg/topgg.py diff --git a/bot/modules/__init__.py b/bot/modules/__init__.py index b1763cae..20630a1b 100644 --- a/bot/modules/__init__.py +++ b/bot/modules/__init__.py @@ -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 * diff --git a/bot/modules/topgg/__init__.py b/bot/modules/topgg/__init__.py new file mode 100644 index 00000000..d9785b83 --- /dev/null +++ b/bot/modules/topgg/__init__.py @@ -0,0 +1,3 @@ +from .module import module + +from . import topgg diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py new file mode 100644 index 00000000..783d98a9 --- /dev/null +++ b/bot/modules/topgg/module.py @@ -0,0 +1,3 @@ +from LionModule import LionModule + +module = LionModule("Topgg") diff --git a/bot/modules/topgg/topgg.py b/bot/modules/topgg/topgg.py new file mode 100644 index 00000000..ad873a84 --- /dev/null +++ b/bot/modules/topgg/topgg.py @@ -0,0 +1,29 @@ +from meta import client +import discord +import topgg + +# client.log("test") + +import topgg + +# This example uses topggpy's webhook system. +client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook("/dblwebhook", "nopassword123") + +# The port must be a number between 1024 and 49151. +client.topgg_webhook.run(5000) # this method can be awaited as well + + +@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}") + if data["type"] == "test": + # this is roughly equivalent to + # `return await on_dbl_test(data)` in this case + 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}") \ No newline at end of file From a2fcdf075feb7d2529c4055bcd5555e5b834d46f Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Fri, 14 Jan 2022 18:49:51 +0530 Subject: [PATCH 04/49] [DB] Init Topgg DB Framework --- bot/modules/topgg/data.py | 8 ++++++++ data/schema.sql | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 bot/modules/topgg/data.py diff --git a/bot/modules/topgg/data.py b/bot/modules/topgg/data.py new file mode 100644 index 00000000..829ce6da --- /dev/null +++ b/bot/modules/topgg/data.py @@ -0,0 +1,8 @@ +from data import RowTable, Table + +topggvotes = RowTable( + 'topgg', + ('voteid', 'userid', 'boostedTimestamp'), + 'voteid' +) + diff --git a/data/schema.sql b/data/schema.sql index 51432086..c5dfca6f 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -144,6 +144,15 @@ CREATE TABLE tasklist( ); CREATE INDEX tasklist_users ON tasklist (userid); +-- Topgg Data {{{ +create TABLE topgg( + voteid SERIAL PRIMARY KEY, + userid BIGINT NOT NULL, + boostedTimestamp TIMESTAMPTZ NOT NULL +); +CREATE INDEX topgg_member ON topgg (guildid, userid); +-- }}} + CREATE TABLE tasklist_channels( guildid BIGINT NOT NULL, channelid BIGINT NOT NULL From 437adf87e4787a9d23b1c2be4dcb4cfc41217ce7 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Fri, 14 Jan 2022 18:52:51 +0530 Subject: [PATCH 05/49] [Context] Implement LionContext Need to append a Text to all bot replies to ask people to !vote - Implement LionContext - Implement Callback handler to enable Modules intercept cmdClient.Context.Utils() (attr calls) --- bot/LionContext.py | 48 +++++++++++++++++++++++++++++++++++ bot/meta/client.py | 5 ++-- bot/utils/interactive.py | 54 ++++++++++++++++++++++++++++++++++------ 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 bot/LionContext.py diff --git a/bot/LionContext.py b/bot/LionContext.py new file mode 100644 index 00000000..cf068d9d --- /dev/null +++ b/bot/LionContext.py @@ -0,0 +1,48 @@ +import datetime + +import discord +from cmdClient import Context +from cmdClient.logger import log + +reply_callbacks: list = [] # TODO Extend to all cmdClient.Context.Utils to give flexibility to modules + +class LionContext(Context): + """ + Subclass to allow easy attachment of custom hooks and structure to contexts. + """ + + def __init__(self, client, **kwargs): + super().__init__(client, **kwargs) + + @classmethod + def util(self, util_func): + """ + Decorator to make a utility function available as a Context instance method + """ + log('added util_function: ' + util_func.__name__) + + def util_fun_wrapper(*args, **kwargs): + [args, kwargs] = self.util_pre(util_func, *args, **kwargs) + return util_func(*args, **kwargs) + + util_fun_wrapper.__name__ = util_func.__name__ # Hack + + super().util(util_fun_wrapper) + + @classmethod + def util_pre(self, util_func, *args, **kwargs): + + if util_func.__name__ == 'reply': + for cb in reply_callbacks: + [args, kwargs] = cb(util_func, *args, **kwargs) # Nesting handlers. Note: args and kwargs are mutable + + return [args, kwargs] + + +def register_reply_callback(func): + reply_callbacks.append(func) + +def unregister_reply_callback(func): + reply_callbacks.remove(func) + + diff --git a/bot/meta/client.py b/bot/meta/client.py index 50414aa8..a3f02877 100644 --- a/bot/meta/client.py +++ b/bot/meta/client.py @@ -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 diff --git a/bot/utils/interactive.py b/bot/utils/interactive.py index 986361d5..a232604e 100644 --- a/bot/utils/interactive.py +++ b/bot/utils/interactive.py @@ -1,8 +1,10 @@ import asyncio import discord -from cmdClient import Context +from LionContext import LionContext from cmdClient.lib import UserCancelled, ResponseTimedOut +import datetime +from cmdClient import lib from .lib import paginate_list # TODO: Interactive locks @@ -19,7 +21,7 @@ async def discord_shield(coro): pass -@Context.util +@LionContext.util async def cancellable(ctx, msg, add_reaction=True, cancel_message=None, timeout=300): """ Add a cancellation reaction to the given message. @@ -62,7 +64,7 @@ async def cancellable(ctx, msg, add_reaction=True, cancel_message=None, timeout= return task -@Context.util +@LionContext.util async def listen_for(ctx, allowed_input=None, timeout=120, lower=True, check=None): """ Listen for a one of a particular set of input strings, @@ -114,7 +116,7 @@ async def listen_for(ctx, allowed_input=None, timeout=120, lower=True, check=Non return message -@Context.util +@LionContext.util async def selector(ctx, header, select_from, timeout=120, max_len=20): """ Interactive routine to prompt the `ctx.author` to select an item from a list. @@ -214,7 +216,7 @@ async def selector(ctx, header, select_from, timeout=120, max_len=20): return result -@Context.util +@LionContext.util async def pager(ctx, pages, locked=True, start_at=0, add_cancel=False, **kwargs): """ Shows the user each page from the provided list `pages` one at a time, @@ -371,7 +373,7 @@ async def _pager(ctx, out_msg, pages, locked, start_at, add_cancel, **kwargs): pass -@Context.util +@LionContext.util async def input(ctx, msg="", timeout=120): """ Listen for a response in the current channel, from ctx.author. @@ -413,7 +415,7 @@ async def input(ctx, msg="", timeout=120): return result -@Context.util +@LionContext.util async def ask(ctx, msg, timeout=30, use_msg=None, del_on_timeout=False): """ Ask ctx.author a yes/no question. @@ -459,3 +461,41 @@ async def ask(ctx, msg, timeout=30, use_msg=None, del_on_timeout=False): if result in ["n", "no"]: return 0 return 1 + +# this reply() will be overide baseContext's reply with LionContext's, whcih can +# hook pre_execution of any util. +# Using this system, Module now have much power to change Context's utils +@LionContext.util +async def reply(ctx, content=None, allow_everyone=False, **kwargs): + """ + Helper function to reply in the current channel. + """ + if not allow_everyone: + if content: + content = lib.sterilise_content(content) + + message = await ctx.ch.send(content=content, **kwargs) + ctx.sent_messages.append(message) + return message + + +# this reply() will be overide baseContext's reply +@LionContext.util +async def error_reply(ctx, error_str): + """ + Notify the user of a user level error. + Typically, this will occur in a red embed, posted in the command channel. + """ + embed = discord.Embed( + colour=discord.Colour.red(), + description=error_str, + timestamp=datetime.datetime.utcnow() + ) + try: + message = await ctx.ch.send(embed=embed) + ctx.sent_messages.append(message) + return message + except discord.Forbidden: + message = await ctx.reply(error_str) + ctx.sent_messages.append(message) + return message \ No newline at end of file From 521dd340bc7f49dc24d4fdd6e8f5e402425a465c Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Fri, 14 Jan 2022 18:54:58 +0530 Subject: [PATCH 06/49] [Topgg] Update module - Implement Sending DM to user - Implement launch and unload tasks to register reply Ctx.Util calls - Implement reply callback handler - Implement !focevote cmd to simulate vote --- bot/modules/topgg/topgg.py | 113 ++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/bot/modules/topgg/topgg.py b/bot/modules/topgg/topgg.py index ad873a84..2ddf0ae1 100644 --- a/bot/modules/topgg/topgg.py +++ b/bot/modules/topgg/topgg.py @@ -1,10 +1,12 @@ from meta import client import discord import topgg +import datetime -# client.log("test") - -import topgg +from .module import module +from wards import guild_admin +from cmdClient.Context import Context +from . import data as db # This example uses topggpy's webhook system. client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook("/dblwebhook", "nopassword123") @@ -16,14 +18,107 @@ client.topgg_webhook.run(5000) # this method can be awaited as well @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}") - if data["type"] == "test": - # this is roughly equivalent to - # `return await on_dbl_test(data)` in this case - return client.dispatch("dbl_test", data) + client.log(f"Received a vote: \n{data}") + db.topggvotes.insert( + userid=data['user'], + boostedTimestamp = datetime.datetime.utcnow() + ) + + await send_user_dm(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}") \ No newline at end of file + client.log(f"Received a test vote:\n{data}") + + +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: + await user.send("Thankyou for upvoting.\n https://cdn.discordapp.com/attachments/908283085999706153/930559064323268618/unknown.png") + except discord.HTTPException: + # Nothing we can really do here. Maybe tell the user about their reminder next time? + pass + + +from LionContext import register_reply_callback, unregister_reply_callback + +@module.launch_task +async def register_hook(client): + client.log("register_reply_hook " ) + + register_reply_callback(reply) + + +@module.unload_task +async def unregister_hook(client): + client.log("register_reply_hook " ) + + unregister_reply_callback(reply) + + +def reply(util_func, *args, **kwargs): + # *args will have LionContext + # **kwargs should have the actual reply() call's extra arguments + + args = list(args) + + if 'embed' in kwargs: + kwargs['embed'].add_field( + name="\u200b", + value=( + f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + ), + inline=False + ) + elif 'content' in args and args['content']: + args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + elif len(args) > 1: + args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + else: + args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + + args = tuple(args) + client.log('test') + + return [args, kwargs] + + +@module.cmd( + "forcevote", + desc="Simulate Topgg Vote.", + group="Guild Admin", + aliases=('debugvote', 'topggvote') +) +@guild_admin() +async def cmd_forcevote(ctx): + """ + Usage``: + {prefix}forcevote + Description: + Simulate Topgg 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)) + \ No newline at end of file From 390315e940dcf936b01a1ef29de6b5eeb1bb8c94 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 17:36:24 +0200 Subject: [PATCH 07/49] fix (setprofile): Disallow editing tag `0`. --- bot/modules/stats/setprofile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/modules/stats/setprofile.py b/bot/modules/stats/setprofile.py index 9618b388..550d793a 100644 --- a/bot/modules/stats/setprofile.py +++ b/bot/modules/stats/setprofile.py @@ -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( From e220ab9dfd6ee254adbf9fa8a366a7ce180e28bc Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 17:55:32 +0200 Subject: [PATCH 08/49] fix (LionModule): Always pre-fetch lion. --- bot/LionModule.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/LionModule.py b/bot/LionModule.py index 9c2a4671..c22a2ab6 100644 --- a/bot/LionModule.py +++ b/bot/LionModule.py @@ -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 + ctx.alion + # Start typing await ctx.ch.trigger_typing() From b124cf8e1e7894243c2c4906b3071cd57c29b484 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 18:30:10 +0200 Subject: [PATCH 09/49] (timers): Destroy timers when channel is gone. --- bot/modules/study/timers/Timer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/modules/study/timers/Timer.py b/bot/modules/study/timers/Timer.py index bef4aa72..88a15f4f 100644 --- a/bot/modules/study/timers/Timer.py +++ b/bot/modules/study/timers/Timer.py @@ -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: From beb7a737d4796c085d3c63cd71458134ca201670 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 18:59:28 +0200 Subject: [PATCH 10/49] (rooms): Fetch guilds from other shards. Manually requests guilds from other shards in `rooms` display. --- bot/modules/accountability/commands.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/bot/modules/accountability/commands.py b/bot/modules/accountability/commands.py index a0d19509..e389f4ea 100644 --- a/bot/modules/accountability/commands.py +++ b/bot/modules/accountability/commands.py @@ -525,11 +525,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,11 +550,11 @@ 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" From 50da6b18d2b5d4093c984d8397075d712cb04e4e Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 19:38:21 +0200 Subject: [PATCH 11/49] (sessions): Account for rounding error in expiry. Also make `_expiry_task` properly cancellable. Fixes an issue where `schedule_expiry` would always reschedule itself. --- bot/modules/study/tracking/session_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/modules/study/tracking/session_tracker.py b/bot/modules/study/tracking/session_tracker.py index df695a36..add30e6a 100644 --- a/bot/modules/study/tracking/session_tracker.py +++ b/bot/modules/study/tracking/session_tracker.py @@ -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? From 24791867cfd7c4598af1951b7ef884e9b6d43ebe Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 14 Jan 2022 19:45:24 +0200 Subject: [PATCH 12/49] tweaks: Update setting defaults. `video_grace_period` now 90 seconds. `task_limit` now 99. `rent_member_limit` now 24. Also updated `Integer` default `_min` and `_max` to match DB values. --- bot/modules/moderation/video/admin.py | 2 +- bot/modules/renting/admin.py | 2 +- bot/modules/todo/admin.py | 2 +- bot/settings/setting_types.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/modules/moderation/video/admin.py b/bot/modules/moderation/video/admin.py index 8d658c60..eb08f69d 100644 --- a/bot/modules/moderation/video/admin.py +++ b/bot/modules/moderation/video/admin.py @@ -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 diff --git a/bot/modules/renting/admin.py b/bot/modules/renting/admin.py index 2427e136..826910a8 100644 --- a/bot/modules/renting/admin.py +++ b/bot/modules/renting/admin.py @@ -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." diff --git a/bot/modules/todo/admin.py b/bot/modules/todo/admin.py index 5e64c41d..0364a817 100644 --- a/bot/modules/todo/admin.py +++ b/bot/modules/todo/admin.py @@ -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." diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index 74a64622..805a28a8 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -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): From 6e005cf0427acec42febddd71df587a6b04b7fc7 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Sat, 15 Jan 2022 16:02:52 +0530 Subject: [PATCH 13/49] [Data-Interface] Allow single column selection --- bot/data/formatters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/data/formatters.py b/bot/data/formatters.py index 4bdccbc3..1509ed08 100644 --- a/bot/data/formatters.py +++ b/bot/data/formatters.py @@ -69,6 +69,8 @@ def _format_selectkeys(keys): """ if not keys: return "*" + elif type(keys) is str: + return keys else: return ", ".join(keys) From b92866f95481db65817b03340de4f75857e29187 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Sat, 15 Jan 2022 16:06:08 +0530 Subject: [PATCH 14/49] Implement !vote and update reply cbHandler - Also init'ed Remind me framework --- bot/modules/topgg/data.py | 2 +- bot/modules/topgg/topgg.py | 83 +++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/bot/modules/topgg/data.py b/bot/modules/topgg/data.py index 829ce6da..fef6153a 100644 --- a/bot/modules/topgg/data.py +++ b/bot/modules/topgg/data.py @@ -1,4 +1,4 @@ -from data import RowTable, Table +from data.interfaces import RowTable, Table topggvotes = RowTable( 'topgg', diff --git a/bot/modules/topgg/topgg.py b/bot/modules/topgg/topgg.py index 2ddf0ae1..fd204324 100644 --- a/bot/modules/topgg/topgg.py +++ b/bot/modules/topgg/topgg.py @@ -1,3 +1,4 @@ +from bot.cmdClient.checks.global_perms import in_guild from meta import client import discord import topgg @@ -7,6 +8,7 @@ from .module import module from wards import guild_admin from cmdClient.Context import Context from . import data as db +from data.conditions import GEQ # This example uses topggpy's webhook system. client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook("/dblwebhook", "nopassword123") @@ -26,6 +28,7 @@ async def on_dbl_vote(data): ) await send_user_dm(data['user']) + check_remainder_settings(data['user']) if data["type"] == "test": return client.dispatch("dbl_test", data) @@ -72,25 +75,34 @@ def reply(util_func, *args, **kwargs): # *args will have LionContext # **kwargs should have the actual reply() call's extra arguments - args = list(args) + author = args[0].author.id - if 'embed' in kwargs: - kwargs['embed'].add_field( - name="\u200b", - value=( - f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - ), - inline=False - ) - elif 'content' in args and args['content']: - args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - elif len(args) > 1: - args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - else: - args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + if not db.topggvotes.select_one_where( + userid=author, + # start_at=LEQ(utc_now() - datetime.timedelta(hours=1)), + # start_at=LEQ(utc_now()), + select_columns="boostedTimestamp", + boostedTimestamp=GEQ(datetime.datetime.utcnow() - datetime.timedelta(hours=12.5)), + _extra="ORDER BY boostedTimestamp DESC LIMIT 1" + ): + args = list(args) + if 'embed' in kwargs: + kwargs['embed'].add_field( + name="\u200b", + value=( + f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + ), + inline=False + ) + elif 'content' in args and args['content']: + args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + elif len(args) > 1: + args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + else: + args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - args = tuple(args) - client.log('test') + args = tuple(args) + client.log('test') return [args, kwargs] @@ -121,4 +133,39 @@ async def cmd_forcevote(ctx): await on_dbl_vote({"user": target.id, "type": "test"}) return await ctx.reply('Topgg vote simulation successful on {}'.format(target)) - \ No newline at end of file + + +@module.cmd( + "vote", + desc="Get Top.gg bot's link for Economy boost.", + group="Economy", + aliases=('topgg', 'topggvote', 'upvote') +) +@in_guild() +async def cmd_vote(ctx): + """ + Usage``: + {prefix}vote + Description: + Get Top.gg bot's link for +25% Economy boost. + """ + target = ctx.author + + return await ctx.reply('My Top.gg vote link is here: https://top.gg/bot/889078613817831495/vote \nThanks!') + + +@module.cmd( + "remind_vote", + group="Personal Settings", + desc="Turn on/off DM Remainder to Upvote me.", + long_help="Use this setting to enable/disable DM remainders from me to upvote on Top.gg." +) +async def cmd_remind_vote(ctx): + """ + Usage``: + {prefix}remind_vote on + {prefix}remind_vote off + + Use this setting to enable/disable DM remainders from me to upvote on Top.gg. + """ + pass \ No newline at end of file From 36a98b1224fa1b9231ec64bc06611a3a1593be77 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:08:25 +0530 Subject: [PATCH 15/49] [Remainders] Update - Send no context if None type is present --- bot/modules/reminders/reminder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/modules/reminders/reminder.py b/bot/modules/reminders/reminder.py index 67956a1d..163c7636 100644 --- a/bot/modules/reminders/reminder.py +++ b/bot/modules/reminders/reminder.py @@ -155,7 +155,9 @@ class Reminder: 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( From 0bb96aeeb21275a504a6100bf7224db123301249 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:10:25 +0530 Subject: [PATCH 16/49] [DB][CORE][Topgg] Update DB to accommodate Topgg module --- bot/core/data.py | 2 +- bot/modules/reminders/data.py | 2 +- bot/modules/topgg/data.py | 2 +- data/migration/v8-v9/migration.sql | 72 ++++++++++++++++++++++++++++++ data/schema.sql | 44 +++++++++++++----- 5 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 data/migration/v8-v9/migration.sql diff --git a/bot/core/data.py b/bot/core/data.py index 9b7f0bdd..41836a3c 100644 --- a/bot/core/data.py +++ b/bot/core/data.py @@ -14,7 +14,7 @@ meta = RowTable( user_config = RowTable( 'user_config', - ('userid', 'timezone'), + ('userid', 'timezone', 'remaind_upvote'), 'userid', cache=TTLCache(5000, ttl=60*5) ) diff --git a/bot/modules/reminders/data.py b/bot/modules/reminders/data.py index 3c57c6bb..bf8f75fc 100644 --- a/bot/modules/reminders/data.py +++ b/bot/modules/reminders/data.py @@ -1,4 +1,4 @@ -from data import RowTable +from data.interfaces import RowTable reminders = RowTable( diff --git a/bot/modules/topgg/data.py b/bot/modules/topgg/data.py index fef6153a..b12a4120 100644 --- a/bot/modules/topgg/data.py +++ b/bot/modules/topgg/data.py @@ -1,4 +1,4 @@ -from data.interfaces import RowTable, Table +from data.interfaces import RowTable topggvotes = RowTable( 'topgg', diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql new file mode 100644 index 00000000..63d148c0 --- /dev/null +++ b/data/migration/v8-v9/migration.sql @@ -0,0 +1,72 @@ +ALTER TABLE user_config + ADD COLUMN remaind_upvote BOOLEAN DEFAULT TRUE + +-- Topgg Data {{{ +CREATE TABLE IF NOT EXISTS topgg( + voteid SERIAL PRIMARY KEY, + userid BIGINT NOT NULL, + boostedTimestamp TIMESTAMPTZ NOT NULL +); +CREATE INDEX topgg_member ON topgg (guildid, userid); +-- }}} + +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; +-- }}} + +INSERT INTO VersionHistory (version, author) VALUES (8, 'v8-v9 migration'); \ No newline at end of file diff --git a/data/schema.sql b/data/schema.sql index c5dfca6f..c73d8749 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -41,7 +41,8 @@ CREATE TABLE global_guild_blacklist( -- User configuration data {{{ CREATE TABLE user_config( userid BIGINT PRIMARY KEY, - timezone TEXT + timezone TEXT, + remaind_upvote BOOLEAN DEFAULT TRUE ); -- }}} @@ -144,15 +145,6 @@ CREATE TABLE tasklist( ); CREATE INDEX tasklist_users ON tasklist (userid); --- Topgg Data {{{ -create TABLE topgg( - voteid SERIAL PRIMARY KEY, - userid BIGINT NOT NULL, - boostedTimestamp TIMESTAMPTZ NOT NULL -); -CREATE INDEX topgg_member ON topgg (guildid, userid); --- }}} - CREATE TABLE tasklist_channels( guildid BIGINT NOT NULL, channelid BIGINT NOT NULL @@ -521,6 +513,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, @@ -529,8 +540,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 @@ -775,4 +786,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_member ON topgg (guildid, userid); +-- }}} + -- vim: set fdm=marker: From e526503bac1a733d4c17ea97808d5686ef378b7b Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:11:10 +0530 Subject: [PATCH 17/49] [Conf] Topgg specifc settings --- config/example-bot.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/example-bot.conf b/config/example-bot.conf index d2ec5dd1..cdaaf501 100644 --- a/config/example-bot.conf +++ b/config/example-bot.conf @@ -14,3 +14,7 @@ data_appid = LionBot shard_count = 1 lion_sync_period = 60 + +topgg_password = +topgg_route = +topgg_port = \ No newline at end of file From 724a39a940a6760311b8b513497ff9424f110fd1 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:14:18 +0530 Subject: [PATCH 18/49] [Topgg] Finalize module framework --- bot/modules/topgg/__init__.py | 5 +- bot/modules/topgg/module.py | 45 +++++++++ bot/modules/topgg/topgg.py | 171 ---------------------------------- 3 files changed, 49 insertions(+), 172 deletions(-) delete mode 100644 bot/modules/topgg/topgg.py diff --git a/bot/modules/topgg/__init__.py b/bot/modules/topgg/__init__.py index d9785b83..bf762868 100644 --- a/bot/modules/topgg/__init__.py +++ b/bot/modules/topgg/__init__.py @@ -1,3 +1,6 @@ from .module import module -from . import topgg +from . import webhook +from . import commands +from . import data +from . import settings \ No newline at end of file diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 783d98a9..b6fb43f2 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -1,3 +1,48 @@ from LionModule import LionModule +from LionContext import register_reply_callback, unregister_reply_callback +from meta.client import client + +from .utils import * +from .webhook import init_webhook module = LionModule("Topgg") + +@module.launch_task +async def register_hook(client): + client.log("register_reply_hook " ) + + init_webhook() + register_reply_callback(reply) + +@module.unload_task +async def unregister_hook(client): + client.log("register_reply_hook " ) + + unregister_reply_callback(reply) + + +def reply(util_func, *args, **kwargs): + # *args will have LionContext + # **kwargs should have the actual reply() call's extra arguments + + if not get_last_voted_timestamp(args[0].author.id): + args = list(args) + if 'embed' in kwargs: + kwargs['embed'].add_field( + name="\u200b", + value=( + f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + ), + inline=False + ) + elif 'content' in args and args['content']: + args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + elif len(args) > 1: + args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + else: + args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + + args = tuple(args) + client.log('test') + + return [args, kwargs] diff --git a/bot/modules/topgg/topgg.py b/bot/modules/topgg/topgg.py deleted file mode 100644 index fd204324..00000000 --- a/bot/modules/topgg/topgg.py +++ /dev/null @@ -1,171 +0,0 @@ -from bot.cmdClient.checks.global_perms import in_guild -from meta import client -import discord -import topgg -import datetime - -from .module import module -from wards import guild_admin -from cmdClient.Context import Context -from . import data as db -from data.conditions import GEQ - -# This example uses topggpy's webhook system. -client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook("/dblwebhook", "nopassword123") - -# The port must be a number between 1024 and 49151. -client.topgg_webhook.run(5000) # this method can be awaited as well - - -@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}") - - db.topggvotes.insert( - userid=data['user'], - boostedTimestamp = datetime.datetime.utcnow() - ) - - await send_user_dm(data['user']) - check_remainder_settings(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}") - - -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: - await user.send("Thankyou for upvoting.\n https://cdn.discordapp.com/attachments/908283085999706153/930559064323268618/unknown.png") - except discord.HTTPException: - # Nothing we can really do here. Maybe tell the user about their reminder next time? - pass - - -from LionContext import register_reply_callback, unregister_reply_callback - -@module.launch_task -async def register_hook(client): - client.log("register_reply_hook " ) - - register_reply_callback(reply) - - -@module.unload_task -async def unregister_hook(client): - client.log("register_reply_hook " ) - - unregister_reply_callback(reply) - - -def reply(util_func, *args, **kwargs): - # *args will have LionContext - # **kwargs should have the actual reply() call's extra arguments - - author = args[0].author.id - - if not db.topggvotes.select_one_where( - userid=author, - # start_at=LEQ(utc_now() - datetime.timedelta(hours=1)), - # start_at=LEQ(utc_now()), - select_columns="boostedTimestamp", - boostedTimestamp=GEQ(datetime.datetime.utcnow() - datetime.timedelta(hours=12.5)), - _extra="ORDER BY boostedTimestamp DESC LIMIT 1" - ): - args = list(args) - if 'embed' in kwargs: - kwargs['embed'].add_field( - name="\u200b", - value=( - f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - ), - inline=False - ) - elif 'content' in args and args['content']: - args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - elif len(args) > 1: - args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - else: - args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" - - args = tuple(args) - client.log('test') - - return [args, kwargs] - - -@module.cmd( - "forcevote", - desc="Simulate Topgg Vote.", - group="Guild Admin", - aliases=('debugvote', 'topggvote') -) -@guild_admin() -async def cmd_forcevote(ctx): - """ - Usage``: - {prefix}forcevote - Description: - Simulate Topgg 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)) - - -@module.cmd( - "vote", - desc="Get Top.gg bot's link for Economy boost.", - group="Economy", - aliases=('topgg', 'topggvote', 'upvote') -) -@in_guild() -async def cmd_vote(ctx): - """ - Usage``: - {prefix}vote - Description: - Get Top.gg bot's link for +25% Economy boost. - """ - target = ctx.author - - return await ctx.reply('My Top.gg vote link is here: https://top.gg/bot/889078613817831495/vote \nThanks!') - - -@module.cmd( - "remind_vote", - group="Personal Settings", - desc="Turn on/off DM Remainder to Upvote me.", - long_help="Use this setting to enable/disable DM remainders from me to upvote on Top.gg." -) -async def cmd_remind_vote(ctx): - """ - Usage``: - {prefix}remind_vote on - {prefix}remind_vote off - - Use this setting to enable/disable DM remainders from me to upvote on Top.gg. - """ - pass \ No newline at end of file From 61d192e27b5486a936f24d42b899b77b2eda787c Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:16:14 +0530 Subject: [PATCH 19/49] Update module.py - specify logging context --- bot/modules/topgg/module.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index b6fb43f2..83a4069d 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -1,3 +1,4 @@ +from multiprocessing import context from LionModule import LionModule from LionContext import register_reply_callback, unregister_reply_callback from meta.client import client @@ -9,16 +10,16 @@ module = LionModule("Topgg") @module.launch_task async def register_hook(client): - client.log("register_reply_hook " ) - init_webhook() register_reply_callback(reply) + + client.log("Registered LionContext reply util hook.", context="Topgg" ) @module.unload_task async def unregister_hook(client): - client.log("register_reply_hook " ) - unregister_reply_callback(reply) + + client.log("Unregistered LionContext reply util hook.", context="Topgg" ) def reply(util_func, *args, **kwargs): @@ -43,6 +44,5 @@ def reply(util_func, *args, **kwargs): args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" args = tuple(args) - client.log('test') return [args, kwargs] From ce59a8cc3f821573b8901563edc30ecef77534a8 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:18:35 +0530 Subject: [PATCH 20/49] [Topgg] Webhook implementation --- bot/modules/topgg/webhook.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 bot/modules/topgg/webhook.py diff --git a/bot/modules/topgg/webhook.py b/bot/modules/topgg/webhook.py new file mode 100644 index 00000000..eb0eb1c5 --- /dev/null +++ b/bot/modules/topgg/webhook.py @@ -0,0 +1,36 @@ +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 * + +@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")) From 9615f2037e136afee68605a4cc3aa09eefb9a5cb Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:18:59 +0530 Subject: [PATCH 21/49] [Topgg] Commands, Settings and Utilities --- bot/modules/topgg/commands.py | 75 +++++++++++++++++++++++++++++++++++ bot/modules/topgg/settings.py | 45 +++++++++++++++++++++ bot/modules/topgg/utils.py | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 bot/modules/topgg/commands.py create mode 100644 bot/modules/topgg/settings.py create mode 100644 bot/modules/topgg/utils.py diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py new file mode 100644 index 00000000..f473fbe0 --- /dev/null +++ b/bot/modules/topgg/commands.py @@ -0,0 +1,75 @@ +import discord +from .module import module +from wards import guild_admin +from bot.cmdClient.checks.global_perms import in_guild +from settings.user_settings import UserSettings + +from .webhook import on_dbl_vote + +@module.cmd( + "forcevote", + desc="Simulate Topgg Vote.", + group="Guild Admin", + aliases=('debugvote', 'topggvote') +) +@guild_admin() +async def cmd_forcevote(ctx): + """ + Usage``: + {prefix}forcevote + Description: + Simulate Topgg 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)) + + +@module.cmd( + "vote", + desc="Get Top.gg bot's link for Economy boost.", + group="Economy", + aliases=('topgg', 'topggvote', 'upvote') +) +@in_guild() +async def cmd_vote(ctx): + """ + Usage``: + {prefix}vote + Description: + Get Top.gg bot's link for +25% Economy boost. + """ + target = ctx.author + + embed=discord.Embed( + title="Topgg Upvote", + description='Please click [here](https://top.gg/bot/889078613817831495/vote) to upvote.\n\nThanks.', + colour=discord.Colour.orange() + ).set_thumbnail( + url="https://cdn.discordapp.com/attachments/908283085999706153/930851470994182144/lionlogo.png" + ) + return await ctx.reply(embed=embed) + + +@module.cmd( + "vote_remainder", + group="Personal Settings", + desc="Turn on/off DM Remainder to Upvote me." +) +async def cmd_remind_vote(ctx): + """ + Usage``: + {prefix}vote_remainder on + {prefix}vote_remainder off + + Use this setting to enable/disable DM remainders from me to upvote on Top.gg. + """ + await UserSettings.settings.vote_remainder.command(ctx, ctx.author.id) diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py new file mode 100644 index 00000000..e60595b3 --- /dev/null +++ b/bot/modules/topgg/settings.py @@ -0,0 +1,45 @@ +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 * + +@UserSettings.attach_setting +class topgg_vote_remainder(Boolean, UserSetting): + attr_name = 'vote_remainder' + _data_column = 'remaind_upvote' + + _default = True + + display_name = 'Upvote Remainder' + desc = "Turn on/off DM Remainders to Upvote me." + long_desc = "Use this setting to enable/disable DM remainders from me to upvote on Top.gg." + + @property + def success_response(self): + if self.value: + # Check if reminder is already running + create_remainder(self.id) + + return ( + "Remainder, in your DMs, to upvote me are {}.\n\n" + "`Do make sure that I can DM you.`" + ).format(self.formatted) + 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 ( + "Remainder, in your DMs, to upvote me are Off." + ).format(self.formatted) \ No newline at end of file diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py new file mode 100644 index 00000000..f50c6da5 --- /dev/null +++ b/bot/modules/topgg/utils.py @@ -0,0 +1,72 @@ +from email.mime import image +import discord +import datetime +from meta.client import client +from bot.settings.setting_types import Integer +from meta import sharding + +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 Upvote me again in Top.gg. \nMy Upvote link is {}".format(topgg_upvote_link) + +# 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 +def get_last_voted_timestamp(userid: Integer): + return db.topggvotes.select_one_where( + userid=userid, + select_columns="boostedTimestamp", + boostedTimestamp=GEQ(datetime.datetime.utcnow() - datetime.timedelta(hours=12.5)), + _extra="ORDER BY boostedTimestamp DESC LIMIT 1" + ) + +# Checks if a remainder is already running (immaterial of remind_at time) +# If no remainder exists creates a new remainder and schedules it +def create_remainder(userid): + 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, + content=remainder_content, + message_link=None, + interval=None, + #remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) + remind_at=last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + ) + + # 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="Thankyou.", + description='Thankyou for upvoting.', + colour=discord.Colour.orange() + ).set_image( + url="https://cdn.discordapp.com/attachments/908283085999706153/930559064323268618/unknown.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 From c01de167f32c1a6e948ce119289c73026287d144 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 00:48:01 +0530 Subject: [PATCH 22/49] [DB] Update schema --- data/migration/v8-v9/migration.sql | 2 +- data/schema.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql index 63d148c0..88466e4c 100644 --- a/data/migration/v8-v9/migration.sql +++ b/data/migration/v8-v9/migration.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS topgg( userid BIGINT NOT NULL, boostedTimestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX topgg_member ON topgg (guildid, userid); +CREATE INDEX topgg_member ON topgg (userid); -- }}} DROP FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT); diff --git a/data/schema.sql b/data/schema.sql index c73d8749..5000b59f 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -792,7 +792,7 @@ create TABLE topgg( userid BIGINT NOT NULL, boostedTimestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX topgg_member ON topgg (guildid, userid); +CREATE INDEX topgg_member ON topgg (userid); -- }}} -- vim: set fdm=marker: From 19816198b0897ed619035aa21cc769e150b28cd1 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 21:12:48 +0530 Subject: [PATCH 23/49] [Reminder] Allow footer and title modification Module will be more more flexible --- bot/modules/reminders/data.py | 2 +- bot/modules/reminders/reminder.py | 5 ++++- data/migration/v8-v9/migration.sql | 4 ++++ data/schema.sql | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bot/modules/reminders/data.py b/bot/modules/reminders/data.py index bf8f75fc..ee5b1a41 100644 --- a/bot/modules/reminders/data.py +++ b/bot/modules/reminders/data.py @@ -3,6 +3,6 @@ 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' ) diff --git a/bot/modules/reminders/reminder.py b/bot/modules/reminders/reminder.py index 163c7636..15772b6a 100644 --- a/bot/modules/reminders/reminder.py +++ b/bot/modules/reminders/reminder.py @@ -150,7 +150,7 @@ 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() @@ -167,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) diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql index 88466e4c..5a46d5e1 100644 --- a/data/migration/v8-v9/migration.sql +++ b/data/migration/v8-v9/migration.sql @@ -1,6 +1,10 @@ ALTER TABLE user_config ADD COLUMN remaind_upvote BOOLEAN DEFAULT TRUE +ALTER TABLE reminders + ADD COLUMN title TEXT DEFAULT NULL, + ADD COLUMN footer TEXT DEFAULT NULL + -- Topgg Data {{{ CREATE TABLE IF NOT EXISTS topgg( voteid SERIAL PRIMARY KEY, diff --git a/data/schema.sql b/data/schema.sql index 5000b59f..4ee4378d 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -167,7 +167,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 DEFAULT NULL, + footer TEXT DEFAULT NULL ); CREATE INDEX reminder_users ON reminders (userid); -- }}} From 417e31c7ab9ecb17599e114bd3c8dfe32ec48294 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 21:14:05 +0530 Subject: [PATCH 24/49] Finalize Topgg bot messages --- bot/modules/topgg/commands.py | 7 ++++--- bot/modules/topgg/module.py | 13 +++++++++---- bot/modules/topgg/utils.py | 19 +++++++++++++------ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py index f473fbe0..58e41eed 100644 --- a/bot/modules/topgg/commands.py +++ b/bot/modules/topgg/commands.py @@ -5,6 +5,7 @@ from bot.cmdClient.checks.global_perms import in_guild from settings.user_settings import UserSettings from .webhook import on_dbl_vote +from .utils import * @module.cmd( "forcevote", @@ -50,11 +51,11 @@ async def cmd_vote(ctx): target = ctx.author embed=discord.Embed( - title="Topgg Upvote", - description='Please click [here](https://top.gg/bot/889078613817831495/vote) to upvote.\n\nThanks.', + title="Claim your boost!", + description='Please click [here](https://top.gg/bot/889078613817831495/vote) vote and support our bot!\n\nThank you! {}.'.format(lion_loveemote), colour=discord.Colour.orange() ).set_thumbnail( - url="https://cdn.discordapp.com/attachments/908283085999706153/930851470994182144/lionlogo.png" + url="https://cdn.discordapp.com/attachments/908283085999706153/933012309532614666/lion-love.png" ) return await ctx.reply(embed=embed) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 83a4069d..94cef145 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -8,6 +8,8 @@ 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 register_hook(client): init_webhook() @@ -28,20 +30,23 @@ def reply(util_func, *args, **kwargs): if not get_last_voted_timestamp(args[0].author.id): args = list(args) + + upvote_info_formatted = upvote_info.format(lion_yayemote, args[0].best_prefix, lion_loveemote) + if 'embed' in kwargs: kwargs['embed'].add_field( name="\u200b", value=( - f"Upvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + upvote_info_formatted ), inline=False ) elif 'content' in args and args['content']: - args['content'] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + args['content'] += '\n\n' + upvote_info_formatted elif len(args) > 1: - args[1] += "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + args[1] += '\n\n' + upvote_info_formatted else: - args['content'] = "\n\nUpvote me to get 🌟**+25% Economy Boost**🌟 - Use `!vote`" + args['content'] = '\n\n' + upvote_info_formatted args = tuple(args) diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index f50c6da5..d4691730 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -12,7 +12,10 @@ from . import data as db from data.conditions import GEQ topgg_upvote_link = 'https://top.gg/bot/889078613817831495/vote' -remainder_content = "You can now Upvote me again in Top.gg. \nMy Upvote link is {}".format(topgg_upvote_link) +remainder_content = "You can now Upvote me again in Top.gg. \nMy Upvote link is [here]({})".format(topgg_upvote_link) + +lion_loveemote = '<:lionloveemote:933003977656795136>' +lion_yayemote = '<:lionyayemote:933003929229352990>' # 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 @@ -37,11 +40,14 @@ def create_remainder(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, - #remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) - remind_at=last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + title="Your boost is now available! {}".format(lion_yayemote), + footer="to stop reminders, use `{}vote_reminder off` command", + # remind_at=last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) ) # Schedule reminder @@ -59,11 +65,12 @@ async def send_user_dm(userid): if user: try: embed=discord.Embed( - title="Thankyou.", - description='Thankyou for upvoting.', + 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/930559064323268618/unknown.png" + url="https://cdn.discordapp.com/attachments/908283085999706153/932737228440993822/lion-yay.png" ) await user.send(embed=embed) From b4e84169005330404f6a31f7212abef218ca0053 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 21:43:50 +0530 Subject: [PATCH 25/49] Final *finalize* UI Texts and some typos --- bot/modules/topgg/commands.py | 14 +++++++------- bot/modules/topgg/settings.py | 16 ++++++++-------- bot/modules/topgg/utils.py | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py index 58e41eed..9bb63cd8 100644 --- a/bot/modules/topgg/commands.py +++ b/bot/modules/topgg/commands.py @@ -36,7 +36,7 @@ async def cmd_forcevote(ctx): @module.cmd( "vote", - desc="Get Top.gg bot's link for Economy boost.", + desc="Get top.gg boost for 25% more LCs.", group="Economy", aliases=('topgg', 'topggvote', 'upvote') ) @@ -61,16 +61,16 @@ async def cmd_vote(ctx): @module.cmd( - "vote_remainder", + "vote_reminder", group="Personal Settings", - desc="Turn on/off DM Remainder to Upvote me." + desc="Turn on/off boost reminders." ) async def cmd_remind_vote(ctx): """ - Usage``: - {prefix}vote_remainder on - {prefix}vote_remainder off + Usage: + `{prefix}vote_reminder on` + `{prefix}vote_reminder off` - Use this setting to enable/disable DM remainders from me to upvote on Top.gg. + Enable or disable DM boost reminders. """ await UserSettings.settings.vote_remainder.command(ctx, ctx.author.id) diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py index e60595b3..2686bc02 100644 --- a/bot/modules/topgg/settings.py +++ b/bot/modules/topgg/settings.py @@ -13,9 +13,9 @@ class topgg_vote_remainder(Boolean, UserSetting): _default = True - display_name = 'Upvote Remainder' - desc = "Turn on/off DM Remainders to Upvote me." - long_desc = "Use this setting to enable/disable DM remainders from me to upvote on Top.gg." + display_name = 'Upvote Reminder' + desc = "Turn on/off DM Reminders to Upvote me." + long_desc = ("Enable or disable DM boost reminders.",) @property def success_response(self): @@ -24,9 +24,9 @@ class topgg_vote_remainder(Boolean, UserSetting): create_remainder(self.id) return ( - "Remainder, in your DMs, to upvote me are {}.\n\n" - "`Do make sure that I can DM you.`" - ).format(self.formatted) + " I will send you boost reminders.\n\n" + "`Please make sure your DMs are open.`" + ) else: # Check if reminder is already running and get its id r = reminders.select_one_where( @@ -41,5 +41,5 @@ class topgg_vote_remainder(Boolean, UserSetting): Reminder.delete(r['reminderid']) return ( - "Remainder, in your DMs, to upvote me are Off." - ).format(self.formatted) \ No newline at end of file + " I won't send you boost reminders." + ) \ No newline at end of file diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index d4691730..0469c839 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -12,7 +12,7 @@ from . import data as db from data.conditions import GEQ topgg_upvote_link = 'https://top.gg/bot/889078613817831495/vote' -remainder_content = "You can now Upvote me again in Top.gg. \nMy Upvote link is [here]({})".format(topgg_upvote_link) +remainder_content = "You can now vote again on top.gg!\nClick [here]({}) to vote, thank you for the support!".format(topgg_upvote_link) lion_loveemote = '<:lionloveemote:933003977656795136>' lion_yayemote = '<:lionyayemote:933003929229352990>' @@ -45,9 +45,9 @@ def create_remainder(userid): message_link=None, interval=None, title="Your boost is now available! {}".format(lion_yayemote), - footer="to stop reminders, use `{}vote_reminder off` command", - # remind_at=last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else datetime.datetime.utcnow() + datetime.timedelta(minutes=5) - remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) + footer="Use `{}vote_reminder off` to stop receiving reminders.", + remind_at=last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + # remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) ) # Schedule reminder From 2c9e62002a927c0fac098a1fee0fa7972f960ccd Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 21:50:22 +0530 Subject: [PATCH 26/49] [Fix] Add prefix in reminders --- bot/modules/topgg/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index 0469c839..233c61b1 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -45,7 +45,7 @@ def create_remainder(userid): message_link=None, interval=None, title="Your boost is now available! {}".format(lion_yayemote), - footer="Use `{}vote_reminder off` to stop receiving reminders.", + 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 datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) ) From 5b4d54a3b92372f582bba01d035b527ea237af37 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 22:41:40 +0530 Subject: [PATCH 27/49] [Core] Expose addCoins to modules --- bot/core/lion.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bot/core/lion.py b/bot/core/lion.py index 4b3baea9..c06fff48 100644 --- a/bot/core/lion.py +++ b/bot/core/lion.py @@ -5,6 +5,8 @@ from meta import client from data import tables as tb from settings import UserSettings, GuildSettings +# Give modules the ability to intercept addCoin() calls +_lion_add_coins_callbacks: list = [] class Lion: """ @@ -214,10 +216,13 @@ 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. """ + for cb in _lion_add_coins_callbacks: + [self, amount, flush, ignorebonus] = cb(self, amount, flush, ignorebonus) + self._pending_coins += amount self._pending[self.key] = self if flush: @@ -251,3 +256,12 @@ class Lion: for lion in lions: lion._pending_coins -= int(lion._pending_coins) cls._pending.pop(lion.key, None) + + +# TODO Expand this callback system to other functions +# Note: callbacks MUST return [self, amount, flush, ignorebonus] modified/unmodified +def register_addcoins_callback(func): + _lion_add_coins_callbacks.append(func) + +def unregister_addcoins_callback(func): + _lion_add_coins_callbacks.remove(func) From 038aabddfa2eaf0e5af99d5f8456e2b80f673a35 Mon Sep 17 00:00:00 2001 From: Harsha Raghu Date: Tue, 18 Jan 2022 22:42:28 +0530 Subject: [PATCH 28/49] [Topgg][Admin][Economy] Implement addCoins cb --- bot/modules/economy/send_cmd.py | 2 +- bot/modules/guild_admin/economy/set_coins.py | 4 ++-- .../guild_admin/reaction_roles/tracker.py | 3 ++- bot/modules/topgg/module.py | 17 ++++++++++++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bot/modules/economy/send_cmd.py b/bot/modules/economy/send_cmd.py index 5086ee67..db997f22 100644 --- a/bot/modules/economy/send_cmd.py +++ b/bot/modules/economy/send_cmd.py @@ -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( diff --git a/bot/modules/guild_admin/economy/set_coins.py b/bot/modules/guild_admin/economy/set_coins.py index c0744a13..cbbf85d9 100644 --- a/bot/modules/guild_admin/economy/set_coins.py +++ b/bot/modules/guild_admin/economy/set_coins.py @@ -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)) diff --git a/bot/modules/guild_admin/reaction_roles/tracker.py b/bot/modules/guild_admin/reaction_roles/tracker.py index 17a64960..bfc43ba5 100644 --- a/bot/modules/guild_admin/reaction_roles/tracker.py +++ b/bot/modules/guild_admin/reaction_roles/tracker.py @@ -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( diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 94cef145..517058f9 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -1,7 +1,9 @@ from multiprocessing import context from LionModule import LionModule from LionContext import register_reply_callback, unregister_reply_callback +from bot.data.conditions import NOT from meta.client import client +from core.lion import register_addcoins_callback, unregister_addcoins_callback from .utils import * from .webhook import init_webhook @@ -14,12 +16,14 @@ upvote_info = "You have a boost available {}, to support our project and earn ** async def register_hook(client): init_webhook() register_reply_callback(reply) - + register_addcoins_callback(cb_addCoins) + client.log("Registered LionContext reply util hook.", context="Topgg" ) @module.unload_task async def unregister_hook(client): unregister_reply_callback(reply) + unregister_addcoins_callback(cb_addCoins) client.log("Unregistered LionContext reply util hook.", context="Topgg" ) @@ -51,3 +55,14 @@ def reply(util_func, *args, **kwargs): args = tuple(args) return [args, kwargs] + + +def cb_addCoins(self, amount, flush, ignorebonus): + + client.log('cb_addCoins hook with amount={} ignorebonux={}'.format(amount, ignorebonus), context='Topgg') + + if not ignorebonus and amount > 0 and get_last_voted_timestamp(self.userid): + amount *= 1.25 + client.log('cb_addCoins with bonus={}'.format(amount), context='Topgg') + + return [self, amount, flush, ignorebonus] \ No newline at end of file From 0948f29e76eddb958139878af1ecdabf1b64d1e5 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 14:14:01 +0200 Subject: [PATCH 29/49] (routine): Update `cmdClient` pointer. --- bot/cmdClient | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cmdClient b/bot/cmdClient index 6eb42690..a6ece4cb 160000 --- a/bot/cmdClient +++ b/bot/cmdClient @@ -1 +1 @@ -Subproject commit 6eb426903423d6be8439621eb0b906aa94957efd +Subproject commit a6ece4cb022ede7ce0d075af333251ed5d41a9b0 From 7fad03ecd94386fe0bb19beaf3cb3047991fab60 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 14:19:38 +0200 Subject: [PATCH 30/49] (data): Update version and index. --- data/migration/v8-v9/migration.sql | 4 ++-- data/schema.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql index 5a46d5e1..3417990b 100644 --- a/data/migration/v8-v9/migration.sql +++ b/data/migration/v8-v9/migration.sql @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS topgg( userid BIGINT NOT NULL, boostedTimestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX topgg_member ON topgg (userid); +CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp); -- }}} DROP FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT); @@ -73,4 +73,4 @@ AS $$ $$ LANGUAGE PLPGSQL; -- }}} -INSERT INTO VersionHistory (version, author) VALUES (8, 'v8-v9 migration'); \ No newline at end of file +INSERT INTO VersionHistory (version, author) VALUES (9, 'v8-v9 migration'); diff --git a/data/schema.sql b/data/schema.sql index 4ee4378d..276179c0 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -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() @@ -794,7 +794,7 @@ create TABLE topgg( userid BIGINT NOT NULL, boostedTimestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX topgg_member ON topgg (userid); +CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp); -- }}} -- vim: set fdm=marker: From 2ff4805d6c6ad34c2cee135bb5aad8aa4b722e68 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 14:54:11 +0200 Subject: [PATCH 31/49] (Lion): Make economy bonus logic explicit. --- bot/core/lion.py | 38 ++++++++++++++++++++++--------------- bot/modules/topgg/module.py | 21 +++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/bot/core/lion.py b/bot/core/lion.py index c06fff48..098ec179 100644 --- a/bot/core/lion.py +++ b/bot/core/lion.py @@ -1,12 +1,11 @@ import pytz +from functools import reduce from datetime import datetime, timedelta from meta import client from data import tables as tb from settings import UserSettings, GuildSettings -# Give modules the ability to intercept addCoin() calls -_lion_add_coins_callbacks: list = [] class Lion: """ @@ -22,6 +21,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 @@ -122,6 +124,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): """ @@ -220,10 +240,7 @@ class Lion: """ Add coins to the user, optionally store the transaction in pending. """ - for cb in _lion_add_coins_callbacks: - [self, amount, flush, ignorebonus] = cb(self, amount, flush, ignorebonus) - - self._pending_coins += amount + self._pending_coins += amount * (1 if ignorebonus else self.economy_bonus) self._pending[self.key] = self if flush: self.flush() @@ -256,12 +273,3 @@ class Lion: for lion in lions: lion._pending_coins -= int(lion._pending_coins) cls._pending.pop(lion.key, None) - - -# TODO Expand this callback system to other functions -# Note: callbacks MUST return [self, amount, flush, ignorebonus] modified/unmodified -def register_addcoins_callback(func): - _lion_add_coins_callbacks.append(func) - -def unregister_addcoins_callback(func): - _lion_add_coins_callbacks.remove(func) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 517058f9..4a88f814 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -3,7 +3,7 @@ from LionModule import LionModule from LionContext import register_reply_callback, unregister_reply_callback from bot.data.conditions import NOT from meta.client import client -from core.lion import register_addcoins_callback, unregister_addcoins_callback +from core.lion import Lion from .utils import * from .webhook import init_webhook @@ -16,15 +16,15 @@ upvote_info = "You have a boost available {}, to support our project and earn ** async def register_hook(client): init_webhook() register_reply_callback(reply) - register_addcoins_callback(cb_addCoins) + Lion.register_economy_bonus(economy_bonus) client.log("Registered LionContext reply util hook.", context="Topgg" ) @module.unload_task async def unregister_hook(client): unregister_reply_callback(reply) - unregister_addcoins_callback(cb_addCoins) - + Lion.unregister_economy_bonus(economy_bonus) + client.log("Unregistered LionContext reply util hook.", context="Topgg" ) @@ -43,7 +43,7 @@ def reply(util_func, *args, **kwargs): value=( upvote_info_formatted ), - inline=False + inline=False ) elif 'content' in args and args['content']: args['content'] += '\n\n' + upvote_info_formatted @@ -57,12 +57,5 @@ def reply(util_func, *args, **kwargs): return [args, kwargs] -def cb_addCoins(self, amount, flush, ignorebonus): - - client.log('cb_addCoins hook with amount={} ignorebonux={}'.format(amount, ignorebonus), context='Topgg') - - if not ignorebonus and amount > 0 and get_last_voted_timestamp(self.userid): - amount *= 1.25 - client.log('cb_addCoins with bonus={}'.format(amount), context='Topgg') - - return [self, amount, flush, ignorebonus] \ No newline at end of file +def economy_bonus(lion): + return 1.25 if get_last_voted_timestamp(lion.userid) else 1 From 153ebcbe3708d2430cdf35910ec7d202acfc5110 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 15:08:27 +0200 Subject: [PATCH 32/49] (topgg): Minor linting and wording changes. --- bot/modules/topgg/commands.py | 28 ++++++++++++++------------ bot/modules/topgg/module.py | 11 +++++----- bot/modules/topgg/settings.py | 5 +++-- bot/modules/topgg/utils.py | 38 +++++++++++++++++++++++++---------- bot/modules/topgg/webhook.py | 16 +++++++++------ 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py index 9bb63cd8..af2a9c3d 100644 --- a/bot/modules/topgg/commands.py +++ b/bot/modules/topgg/commands.py @@ -1,29 +1,30 @@ import discord from .module import module from wards import guild_admin -from bot.cmdClient.checks.global_perms import in_guild +from bot.cmdClient.checks import in_guild, is_owner from settings.user_settings import UserSettings from .webhook import on_dbl_vote -from .utils import * +from .utils import lion_loveemote + @module.cmd( "forcevote", - desc="Simulate Topgg Vote.", - group="Guild Admin", - aliases=('debugvote', 'topggvote') + desc="Simulate a Topgg Vote from the given user.", + group="Bot Admin", ) -@guild_admin() +@is_owner() async def cmd_forcevote(ctx): """ Usage``: {prefix}forcevote Description: - Simulate Topgg Vote without actually a confirmation from Topgg site. + 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: @@ -36,7 +37,7 @@ async def cmd_forcevote(ctx): @module.cmd( "vote", - desc="Get top.gg boost for 25% more LCs.", + desc="[Vote](https://top.gg/bot/889078613817831495/vote) for me to get 25% more LCs!", group="Economy", aliases=('topgg', 'topggvote', 'upvote') ) @@ -48,11 +49,12 @@ async def cmd_vote(ctx): Description: Get Top.gg bot's link for +25% Economy boost. """ - target = ctx.author - - embed=discord.Embed( + embed = discord.Embed( title="Claim your boost!", - description='Please click [here](https://top.gg/bot/889078613817831495/vote) vote and support our bot!\n\nThank you! {}.'.format(lion_loveemote), + 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" @@ -72,5 +74,5 @@ async def cmd_remind_vote(ctx): `{prefix}vote_reminder off` Enable or disable DM boost reminders. - """ + """ await UserSettings.settings.vote_remainder.command(ctx, ctx.author.id) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 4a88f814..2c9f32d5 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -1,31 +1,30 @@ -from multiprocessing import context from LionModule import LionModule from LionContext import register_reply_callback, unregister_reply_callback -from bot.data.conditions import NOT -from meta.client import client from core.lion import Lion -from .utils import * +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 register_hook(client): init_webhook() register_reply_callback(reply) Lion.register_economy_bonus(economy_bonus) - client.log("Registered LionContext reply util hook.", context="Topgg" ) + client.log("Registered LionContext reply util hook.", context="Topgg") + @module.unload_task async def unregister_hook(client): unregister_reply_callback(reply) Lion.unregister_economy_bonus(economy_bonus) - client.log("Unregistered LionContext reply util hook.", context="Topgg" ) + client.log("Unregistered LionContext reply util hook.", context="Topgg") def reply(util_func, *args, **kwargs): diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py index 2686bc02..52cdcdf5 100644 --- a/bot/modules/topgg/settings.py +++ b/bot/modules/topgg/settings.py @@ -4,7 +4,8 @@ from settings.setting_types import Boolean from modules.reminders.reminder import Reminder from modules.reminders.data import reminders -from .utils import * +from .utils import create_remainder, remainder_content + @UserSettings.attach_setting class topgg_vote_remainder(Boolean, UserSetting): @@ -42,4 +43,4 @@ class topgg_vote_remainder(Boolean, UserSetting): return ( " I won't send you boost reminders." - ) \ No newline at end of file + ) diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index 233c61b1..cb801e50 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -12,24 +12,33 @@ 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!\nClick [here]({}) to vote, thank you for the support!".format(topgg_upvote_link) +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 = '<:lionloveemote:933003977656795136>' lion_yayemote = '<:lionyayemote:933003929229352990>' -# 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 + 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", + select_columns="boostedTimestamp", boostedTimestamp=GEQ(datetime.datetime.utcnow() - datetime.timedelta(hours=12.5)), _extra="ORDER BY boostedTimestamp DESC LIMIT 1" ) -# Checks if a remainder is already running (immaterial of remind_at time) -# If no remainder exists creates a new remainder and schedules it + 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, @@ -46,10 +55,14 @@ def create_remainder(userid): 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 datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + remind_at=( + last_vote_time[0] + datetime.timedelta(hours=12.5) + if last_vote_time else + datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + ) # remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) ) - + # Schedule reminder if sharding.shard_number == 0: reminder.schedule() @@ -64,10 +77,13 @@ async def send_user_dm(userid): pass if user: try: - embed=discord.Embed( + 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!", + 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" diff --git a/bot/modules/topgg/webhook.py b/bot/modules/topgg/webhook.py index eb0eb1c5..d26cdd80 100644 --- a/bot/modules/topgg/webhook.py +++ b/bot/modules/topgg/webhook.py @@ -4,7 +4,8 @@ from utils.lib import utc_now from meta.config import conf import topgg -from .utils import * +from .utils import db, send_user_dm, create_remainder + @client.event async def on_dbl_vote(data): @@ -13,17 +14,17 @@ async def on_dbl_vote(data): db.topggvotes.insert( userid=data['user'], - boostedTimestamp = utc_now() + 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): @@ -32,5 +33,8 @@ async def on_dbl_test(data): 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")) + 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")) From a3e6c5419e8011e95d8d8b35f56105b4ed7936b4 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 15:45:17 +0200 Subject: [PATCH 33/49] fix (topgg): Use timezone-aware timestamps. --- bot/modules/topgg/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index cb801e50..165d04db 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -1,9 +1,9 @@ -from email.mime import image import discord import datetime from meta.client import client from bot.settings.setting_types import Integer from meta import sharding +from utils.lib import utc_now from modules.reminders.reminder import Reminder from modules.reminders.data import reminders @@ -29,7 +29,7 @@ def get_last_voted_timestamp(userid: Integer): return db.topggvotes.select_one_where( userid=userid, select_columns="boostedTimestamp", - boostedTimestamp=GEQ(datetime.datetime.utcnow() - datetime.timedelta(hours=12.5)), + boostedTimestamp=GEQ(utc_now() - datetime.timedelta(hours=12.5)), _extra="ORDER BY boostedTimestamp DESC LIMIT 1" ) @@ -58,7 +58,7 @@ def create_remainder(userid): remind_at=( last_vote_time[0] + datetime.timedelta(hours=12.5) if last_vote_time else - datetime.datetime.utcnow() + datetime.timedelta(minutes=5) + utc_now() + datetime.timedelta(minutes=5) ) # remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2) ) From 6f8748a722d755f48a9f0e0aa2779e1b628cbf5d Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 19 Jan 2022 15:59:22 +0200 Subject: [PATCH 34/49] (topgg): Avoid overflowing messages. --- bot/modules/topgg/module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 2c9f32d5..6987294e 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -44,9 +44,9 @@ def reply(util_func, *args, **kwargs): ), inline=False ) - elif 'content' in args and args['content']: + elif 'content' in args and args['content'] and len(args['content']) + len(upvote_info_formatted) < 1998: args['content'] += '\n\n' + upvote_info_formatted - elif len(args) > 1: + elif len(args) > 1 and len(args[1]) + len(upvote_info_formatted) < 1998: args[1] += '\n\n' + upvote_info_formatted else: args['content'] = '\n\n' + upvote_info_formatted From ef81ab7afe566c7957be2148825716db4a3484d2 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 07:48:13 +0200 Subject: [PATCH 35/49] (LCtx): Refactor utility wrapping. --- bot/LionContext.py | 97 ++++++++++++++++++++++++++----------- bot/modules/topgg/module.py | 44 +++++++++-------- bot/utils/interactive.py | 38 --------------- 3 files changed, 91 insertions(+), 88 deletions(-) diff --git a/bot/LionContext.py b/bot/LionContext.py index cf068d9d..4e01b579 100644 --- a/bot/LionContext.py +++ b/bot/LionContext.py @@ -1,48 +1,87 @@ -import datetime +import types -import discord from cmdClient import Context from cmdClient.logger import log -reply_callbacks: list = [] # TODO Extend to all cmdClient.Context.Utils to give flexibility to modules class LionContext(Context): """ Subclass to allow easy attachment of custom hooks and structure to contexts. """ - - def __init__(self, client, **kwargs): - super().__init__(client, **kwargs) - @classmethod - def util(self, util_func): + def util(cls, util_func): """ - Decorator to make a utility function available as a Context instance method + 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. """ - log('added util_function: ' + util_func.__name__) - - def util_fun_wrapper(*args, **kwargs): - [args, kwargs] = self.util_pre(util_func, *args, **kwargs) - return util_func(*args, **kwargs) - - util_fun_wrapper.__name__ = util_func.__name__ # Hack - - super().util(util_fun_wrapper) + super().util(util_func) + log(f"Attached context utility function: {util_func.__name__}") + return util_func @classmethod - def util_pre(self, util_func, *args, **kwargs): - - if util_func.__name__ == 'reply': - for cb in reply_callbacks: - [args, kwargs] = cb(util_func, *args, **kwargs) # Nesting handlers. Note: args and kwargs are mutable - - return [args, kwargs] + 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 -def register_reply_callback(func): - reply_callbacks.append(func) +class Wrappable: + __slots = ('_func', 'wrappers') -def unregister_reply_callback(func): - reply_callbacks.remove(func) + 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): + print(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) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 6987294e..8c8e9ff9 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -1,5 +1,5 @@ from LionModule import LionModule -from LionContext import register_reply_callback, unregister_reply_callback +from LionContext import LionContext from core.lion import Lion from .utils import get_last_voted_timestamp, lion_loveemote, lion_yayemote @@ -10,33 +10,34 @@ 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): - init_webhook() - register_reply_callback(reply) + LionContext.reply.add_wrapper(topgg_reply_wrapper) Lion.register_economy_bonus(economy_bonus) - client.log("Registered LionContext reply util hook.", context="Topgg") + client.log("Loaded top.gg hooks.", context="TOPGG") @module.unload_task async def unregister_hook(client): - unregister_reply_callback(reply) Lion.unregister_economy_bonus(economy_bonus) + LionContext.reply.remove_wrapper(topgg_reply_wrapper.__name__) - client.log("Unregistered LionContext reply util hook.", context="Topgg") + client.log("Unloaded top.gg hooks.", context="TOPGG") -def reply(util_func, *args, **kwargs): - # *args will have LionContext - # **kwargs should have the actual reply() call's extra arguments - - if not get_last_voted_timestamp(args[0].author.id): - args = list(args) - +async def topgg_reply_wrapper(func, *args, suggest_vote=True, **kwargs): + if suggest_vote and 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=( @@ -44,16 +45,17 @@ def reply(util_func, *args, **kwargs): ), inline=False ) - elif 'content' in args and args['content'] and len(args['content']) + len(upvote_info_formatted) < 1998: - args['content'] += '\n\n' + upvote_info_formatted - elif len(args) > 1 and len(args[1]) + len(upvote_info_formatted) < 1998: - args[1] += '\n\n' + upvote_info_formatted else: - args['content'] = '\n\n' + upvote_info_formatted + # 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 - args = tuple(args) - - return [args, kwargs] + return await func(*args, **kwargs) def economy_bonus(lion): diff --git a/bot/utils/interactive.py b/bot/utils/interactive.py index a232604e..01bd49a3 100644 --- a/bot/utils/interactive.py +++ b/bot/utils/interactive.py @@ -461,41 +461,3 @@ async def ask(ctx, msg, timeout=30, use_msg=None, del_on_timeout=False): if result in ["n", "no"]: return 0 return 1 - -# this reply() will be overide baseContext's reply with LionContext's, whcih can -# hook pre_execution of any util. -# Using this system, Module now have much power to change Context's utils -@LionContext.util -async def reply(ctx, content=None, allow_everyone=False, **kwargs): - """ - Helper function to reply in the current channel. - """ - if not allow_everyone: - if content: - content = lib.sterilise_content(content) - - message = await ctx.ch.send(content=content, **kwargs) - ctx.sent_messages.append(message) - return message - - -# this reply() will be overide baseContext's reply -@LionContext.util -async def error_reply(ctx, error_str): - """ - Notify the user of a user level error. - Typically, this will occur in a red embed, posted in the command channel. - """ - embed = discord.Embed( - colour=discord.Colour.red(), - description=error_str, - timestamp=datetime.datetime.utcnow() - ) - try: - message = await ctx.ch.send(embed=embed) - ctx.sent_messages.append(message) - return message - except discord.Forbidden: - message = await ctx.reply(error_str) - ctx.sent_messages.append(message) - return message \ No newline at end of file From 570c91eb94fac322c89357c564238d4f050dc338 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 08:07:01 +0200 Subject: [PATCH 36/49] (topgg): Name and wording tweaks. --- bot/constants.py | 2 +- bot/core/data.py | 2 +- bot/modules/topgg/settings.py | 20 ++++++++++++-------- data/migration/v8-v9/migration.sql | 6 +++--- data/schema.sql | 6 +++--- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 6c122bcc..b3757a74 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -1,2 +1,2 @@ CONFIG_FILE = "config/bot.conf" -DATA_VERSION = 8 +DATA_VERSION = 9 diff --git a/bot/core/data.py b/bot/core/data.py index 41836a3c..3087c8ee 100644 --- a/bot/core/data.py +++ b/bot/core/data.py @@ -14,7 +14,7 @@ meta = RowTable( user_config = RowTable( 'user_config', - ('userid', 'timezone', 'remaind_upvote'), + ('userid', 'timezone', 'topgg_vote_reminder'), 'userid', cache=TTLCache(5000, ttl=60*5) ) diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py index 52cdcdf5..b8800f34 100644 --- a/bot/modules/topgg/settings.py +++ b/bot/modules/topgg/settings.py @@ -4,19 +4,23 @@ 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 +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 = 'remaind_upvote' + _data_column = 'topgg_vote_reminder' _default = True - display_name = 'Upvote Reminder' - desc = "Turn on/off DM Reminders to Upvote me." - long_desc = ("Enable or disable DM boost reminders.",) + 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): @@ -25,8 +29,8 @@ class topgg_vote_remainder(Boolean, UserSetting): create_remainder(self.id) return ( - " I will send you boost reminders.\n\n" - "`Please make sure your DMs are open.`" + "Thank you for supporting me! I will remind in your DMs when you can vote next! " + "(Please make sue your DMs are open, otherwise I can't reach you!)" ) else: # Check if reminder is already running and get its id @@ -42,5 +46,5 @@ class topgg_vote_remainder(Boolean, UserSetting): Reminder.delete(r['reminderid']) return ( - " I won't send you boost reminders." + "I will no longer send you voting reminders." ) diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql index 3417990b..feefd5e5 100644 --- a/data/migration/v8-v9/migration.sql +++ b/data/migration/v8-v9/migration.sql @@ -1,9 +1,9 @@ ALTER TABLE user_config - ADD COLUMN remaind_upvote BOOLEAN DEFAULT TRUE + ADD COLUMN topgg_vote_reminder BOOLEAN; ALTER TABLE reminders - ADD COLUMN title TEXT DEFAULT NULL, - ADD COLUMN footer TEXT DEFAULT NULL + ADD COLUMN title TEXT, + ADD COLUMN footer TEXT; -- Topgg Data {{{ CREATE TABLE IF NOT EXISTS topgg( diff --git a/data/schema.sql b/data/schema.sql index 276179c0..2ba429d7 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -42,7 +42,7 @@ CREATE TABLE global_guild_blacklist( CREATE TABLE user_config( userid BIGINT PRIMARY KEY, timezone TEXT, - remaind_upvote BOOLEAN DEFAULT TRUE + topgg_vote_reminder ); -- }}} @@ -168,8 +168,8 @@ CREATE TABLE reminders( message_link TEXT, interval INTEGER, created_at TIMESTAMP DEFAULT (now() at time zone 'utc'), - title TEXT DEFAULT NULL, - footer TEXT DEFAULT NULL + title TEXT, + footer TEXT ); CREATE INDEX reminder_users ON reminders (userid); -- }}} From dd85732b6ca34182f4a202bd1c9674b9f363f1b0 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 08:17:30 +0200 Subject: [PATCH 37/49] fix (LCtx): Removing debug. --- bot/LionContext.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/LionContext.py b/bot/LionContext.py index 4e01b579..8444c9cc 100644 --- a/bot/LionContext.py +++ b/bot/LionContext.py @@ -61,7 +61,6 @@ class Wrappable: ) def __call__(self, *args, **kwargs): - print(args, kwargs) if self.wrappers: return self._wrapped(iter(self.wrappers.values()))(*args, **kwargs) else: From 4e66e1da806215252668ce1c6c18bf62c7f2ebb2 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 08:22:12 +0200 Subject: [PATCH 38/49] fix (LCtx): Add slots to new Context. Also add `topggpy` requirement. --- bot/LionContext.py | 4 +++- requirements.txt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/LionContext.py b/bot/LionContext.py index 8444c9cc..73ac83a9 100644 --- a/bot/LionContext.py +++ b/bot/LionContext.py @@ -8,6 +8,8 @@ class LionContext(Context): """ Subclass to allow easy attachment of custom hooks and structure to contexts. """ + __slots__ = () + @classmethod def util(cls, util_func): """ @@ -30,7 +32,7 @@ class LionContext(Context): class Wrappable: - __slots = ('_func', 'wrappers') + __slots__ = ('_func', 'wrappers') def __init__(self, func): self._func = func diff --git a/requirements.txt b/requirements.txt index 01a8997c..c9254347 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ discord.py==1.7.3 iso8601==0.1.16 psycopg2==2.9.1 pytz==2021.1 +topggpy From fa1d301cfb371d0ef595b5c5c42d0fc90ba46268 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 08:27:05 +0200 Subject: [PATCH 39/49] fix (topgg): Fix typo. --- bot/modules/topgg/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py index b8800f34..33daad75 100644 --- a/bot/modules/topgg/settings.py +++ b/bot/modules/topgg/settings.py @@ -30,7 +30,7 @@ class topgg_vote_remainder(Boolean, UserSetting): return ( "Thank you for supporting me! I will remind in your DMs when you can vote next! " - "(Please make sue your DMs are open, otherwise I can't reach you!)" + "(Please make sure your DMs are open, otherwise I can't reach you!)" ) else: # Check if reminder is already running and get its id From 49c8ea5e02b64207957644c65f7e7a424dc741e4 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 08:40:44 +0200 Subject: [PATCH 40/49] (config): Add emoji support. --- bot/meta/config.py | 32 ++++++++++++++++++++++++++++++++ bot/modules/topgg/utils.py | 10 ++++++---- config/example-bot.conf | 6 +++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/bot/meta/config.py b/bot/meta/config.py index ca779924..e0913c55 100644 --- a/bot/meta/config.py +++ b/bot/meta/config.py @@ -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 + ` or fallback` + `<:name:id> or fallback` + `` + `<: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", []) diff --git a/bot/modules/topgg/utils.py b/bot/modules/topgg/utils.py index 165d04db..d2b91014 100644 --- a/bot/modules/topgg/utils.py +++ b/bot/modules/topgg/utils.py @@ -1,9 +1,11 @@ import discord import datetime -from meta.client import client -from bot.settings.setting_types import Integer + 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 @@ -17,8 +19,8 @@ remainder_content = ( "Click [here]({}) to vote, thank you for the support!" ).format(topgg_upvote_link) -lion_loveemote = '<:lionloveemote:933003977656795136>' -lion_yayemote = '<:lionyayemote:933003929229352990>' +lion_loveemote = conf.emojis.getemoji('lionlove') +lion_yayemote = conf.emojis.getemoji('lionyay') def get_last_voted_timestamp(userid: Integer): diff --git a/config/example-bot.conf b/config/example-bot.conf index cdaaf501..0ac3142a 100644 --- a/config/example-bot.conf +++ b/config/example-bot.conf @@ -17,4 +17,8 @@ lion_sync_period = 60 topgg_password = topgg_route = -topgg_port = \ No newline at end of file +topgg_port = + +[EMOJIS] +lionyay = +lionlove = From f504177e93e1318c110e899859882cbe544c2875 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 09:26:09 +0200 Subject: [PATCH 41/49] (LCtx): Complete util migration. --- bot/modules/topgg/commands.py | 8 ++++---- bot/utils/ctx_addons.py | 2 +- bot/utils/interactive.py | 16 +++++++--------- bot/utils/seekers.py | 4 ++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py index af2a9c3d..bff229d4 100644 --- a/bot/modules/topgg/commands.py +++ b/bot/modules/topgg/commands.py @@ -1,8 +1,8 @@ import discord from .module import module -from wards import guild_admin 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 @@ -14,7 +14,7 @@ from .utils import lion_loveemote group="Bot Admin", ) @is_owner() -async def cmd_forcevote(ctx): +async def cmd_forcevote(ctx: LionContext): """ Usage``: {prefix}forcevote @@ -42,7 +42,7 @@ async def cmd_forcevote(ctx): aliases=('topgg', 'topggvote', 'upvote') ) @in_guild() -async def cmd_vote(ctx): +async def cmd_vote(ctx: LionContext): """ Usage``: {prefix}vote @@ -67,7 +67,7 @@ async def cmd_vote(ctx): group="Personal Settings", desc="Turn on/off boost reminders." ) -async def cmd_remind_vote(ctx): +async def cmd_remind_vote(ctx: LionContext): """ Usage: `{prefix}vote_reminder on` diff --git a/bot/utils/ctx_addons.py b/bot/utils/ctx_addons.py index 1a01139c..12d89cda 100644 --- a/bot/utils/ctx_addons.py +++ b/bot/utils/ctx_addons.py @@ -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 diff --git a/bot/utils/interactive.py b/bot/utils/interactive.py index 01bd49a3..8f31de7c 100644 --- a/bot/utils/interactive.py +++ b/bot/utils/interactive.py @@ -1,10 +1,8 @@ import asyncio import discord -from LionContext import LionContext +from LionContext import LionContext as Context from cmdClient.lib import UserCancelled, ResponseTimedOut -import datetime -from cmdClient import lib from .lib import paginate_list # TODO: Interactive locks @@ -21,7 +19,7 @@ async def discord_shield(coro): pass -@LionContext.util +@Context.util async def cancellable(ctx, msg, add_reaction=True, cancel_message=None, timeout=300): """ Add a cancellation reaction to the given message. @@ -64,7 +62,7 @@ async def cancellable(ctx, msg, add_reaction=True, cancel_message=None, timeout= return task -@LionContext.util +@Context.util async def listen_for(ctx, allowed_input=None, timeout=120, lower=True, check=None): """ Listen for a one of a particular set of input strings, @@ -116,7 +114,7 @@ async def listen_for(ctx, allowed_input=None, timeout=120, lower=True, check=Non return message -@LionContext.util +@Context.util async def selector(ctx, header, select_from, timeout=120, max_len=20): """ Interactive routine to prompt the `ctx.author` to select an item from a list. @@ -216,7 +214,7 @@ async def selector(ctx, header, select_from, timeout=120, max_len=20): return result -@LionContext.util +@Context.util async def pager(ctx, pages, locked=True, start_at=0, add_cancel=False, **kwargs): """ Shows the user each page from the provided list `pages` one at a time, @@ -373,7 +371,7 @@ async def _pager(ctx, out_msg, pages, locked, start_at, add_cancel, **kwargs): pass -@LionContext.util +@Context.util async def input(ctx, msg="", timeout=120): """ Listen for a response in the current channel, from ctx.author. @@ -415,7 +413,7 @@ async def input(ctx, msg="", timeout=120): return result -@LionContext.util +@Context.util async def ask(ctx, msg, timeout=30, use_msg=None, del_on_timeout=False): """ Ask ctx.author a yes/no question. diff --git a/bot/utils/seekers.py b/bot/utils/seekers.py index 7f3d49d3..e7938d47 100644 --- a/bot/utils/seekers.py +++ b/bot/utils/seekers.py @@ -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 From f2e4cfae8b26d996740ddaf10ef17c53370c2c36 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 10:00:43 +0200 Subject: [PATCH 42/49] text (new_members): `Greeting` -> `Welcome`. --- .../guild_admin/new_members/settings.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/modules/guild_admin/new_members/settings.py b/bot/modules/guild_admin/new_members/settings.py index 26f1c185..8c5cde2c 100644 --- a/bot/modules/guild_admin/new_members/settings.py +++ b/bot/modules/guild_admin/new_members/settings.py @@ -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""" From 0483233c25193770544a7764167b8a3bd68a65f9 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 10:31:44 +0200 Subject: [PATCH 43/49] text (rooms): Migrate to `scheduled sessions`. --- bot/modules/accountability/TimeSlot.py | 22 ++++----- bot/modules/accountability/admin.py | 58 +++++++++++------------ bot/modules/accountability/commands.py | 65 ++++++++++++++------------ bot/modules/accountability/tracker.py | 20 ++++---- 4 files changed, 84 insertions(+), 81 deletions(-) diff --git a/bot/modules/accountability/TimeSlot.py b/bot/modules/accountability/TimeSlot.py index 03ade394..fa2b8f98 100644 --- a/bot/modules/accountability/TimeSlot.py +++ b/bot/modules/accountability/TimeSlot.py @@ -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 .".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( diff --git a/bot/modules/accountability/admin.py b/bot/modules/accountability/admin.py index e294feef..6f72df96 100644 --- a/bot/modules/accountability/admin.py +++ b/bot/modules/accountability/admin.py @@ -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) diff --git a/bot/modules/accountability/commands.py b/bot/modules/accountability/commands.py index e389f4ea..ae0afc69 100644 --- a/bot/modules/accountability/commands.py +++ b/bot/modules/accountability/commands.py @@ -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 ), @@ -558,16 +561,16 @@ async def cmd_rooms(ctx): ) 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!" @@ -576,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, diff --git a/bot/modules/accountability/tracker.py b/bot/modules/accountability/tracker.py index faa82867..21377361 100644 --- a/bot/modules/accountability/tracker.py +++ b/bot/modules/accountability/tracker.py @@ -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() ), From b21224812ecb1da5383627da68073e3cb34e22d3 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 10:33:46 +0200 Subject: [PATCH 44/49] (readme): Remove `Coming Soon` from timers. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b98524f..b1517193 100644 --- a/README.md +++ b/README.md @@ -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! From 4a67736adc76107bd3000e852565964480411a6d Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 12:12:17 +0200 Subject: [PATCH 45/49] (core): Add member name caching. --- bot/LionModule.py | 4 +- bot/core/data.py | 6 ++- bot/core/lion.py | 46 ++++++++++++++++++- bot/modules/study/tracking/session_tracker.py | 2 +- data/migration/v8-v9/migration.sql | 11 +++++ data/schema.sql | 7 ++- 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/bot/LionModule.py b/bot/LionModule.py index c22a2ab6..99618f60 100644 --- a/bot/LionModule.py +++ b/bot/LionModule.py @@ -105,8 +105,8 @@ 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 - ctx.alion + # Ensure Lion exists and cached data is up to date + ctx.alion.update_saved_data(ctx.author) # Start typing await ctx.ch.trigger_typing() diff --git a/bot/core/data.py b/bot/core/data.py index 3087c8ee..b6b0e7b3 100644 --- a/bot/core/data.py +++ b/bot/core/data.py @@ -14,7 +14,7 @@ meta = RowTable( user_config = RowTable( 'user_config', - ('userid', 'timezone', 'topgg_vote_reminder'), + ('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'), diff --git a/bot/core/lion.py b/bot/core/lion.py index 098ec179..969c3e4f 100644 --- a/bot/core/lion.py +++ b/bot/core/lion.py @@ -1,4 +1,5 @@ import pytz +import discord from functools import reduce from datetime import datetime, timedelta @@ -48,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, @@ -75,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): """ @@ -229,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. diff --git a/bot/modules/study/tracking/session_tracker.py b/bot/modules/study/tracking/session_tracker.py index add30e6a..a1392fb2 100644 --- a/bot/modules/study/tracking/session_tracker.py +++ b/bot/modules/study/tracking/session_tracker.py @@ -254,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: diff --git a/data/migration/v8-v9/migration.sql b/data/migration/v8-v9/migration.sql index feefd5e5..b977df75 100644 --- a/data/migration/v8-v9/migration.sql +++ b/data/migration/v8-v9/migration.sql @@ -73,4 +73,15 @@ AS $$ $$ 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'); diff --git a/data/schema.sql b/data/schema.sql index 2ba429d7..bfe51e28 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -42,7 +42,8 @@ CREATE TABLE global_guild_blacklist( CREATE TABLE user_config( userid BIGINT PRIMARY KEY, timezone TEXT, - topgg_vote_reminder + topgg_vote_reminder, + avatar_hash TEXT ); -- }}} @@ -80,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( @@ -405,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) ); From 22f5b90de8736c3af2f84c3bef20463359cab987 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 13:01:28 +0200 Subject: [PATCH 46/49] fix (config): Add the Scheduled Sessions group. --- bot/modules/guild_admin/guild_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/modules/guild_admin/guild_config.py b/bot/modules/guild_admin/guild_config.py index c79d62bb..237c5a49 100644 --- a/bot/modules/guild_admin/guild_config.py +++ b/bot/modules/guild_admin/guild_config.py @@ -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 From d33462dd09f21d7406f96b404fce8ebe0ae277d0 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 13:40:33 +0200 Subject: [PATCH 47/49] (topgg): Add ignores to reply hook. --- bot/modules/topgg/module.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index 8c8e9ff9..a6e6857e 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -33,7 +33,15 @@ async def unregister_hook(client): async def topgg_reply_wrapper(func, *args, suggest_vote=True, **kwargs): - if suggest_vote and not get_last_voted_timestamp(args[0].author.id): + 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: From 31ef615eee0d91542a9df62f0ca5f3f49e16964c Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 18:48:02 +0200 Subject: [PATCH 48/49] (topgg): Exclude `vote` from reply hook. --- bot/modules/topgg/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/modules/topgg/commands.py b/bot/modules/topgg/commands.py index bff229d4..b9ff116c 100644 --- a/bot/modules/topgg/commands.py +++ b/bot/modules/topgg/commands.py @@ -32,7 +32,7 @@ async def cmd_forcevote(ctx: LionContext): 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)) + return await ctx.reply('Topgg vote simulation successful on {}'.format(target), suggest_vote=False) @module.cmd( @@ -59,7 +59,7 @@ async def cmd_vote(ctx: LionContext): ).set_thumbnail( url="https://cdn.discordapp.com/attachments/908283085999706153/933012309532614666/lion-love.png" ) - return await ctx.reply(embed=embed) + return await ctx.reply(embed=embed, suggest_vote=False) @module.cmd( From 2818e9e7bed92f27029c627792c0d52b009a63df Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 20 Jan 2022 22:59:06 +0200 Subject: [PATCH 49/49] (tickets): Increase cache size. --- bot/modules/moderation/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/modules/moderation/data.py b/bot/modules/moderation/data.py index 65b45790..e7f00594 100644 --- a/bot/modules/moderation/data.py +++ b/bot/modules/moderation/data.py @@ -13,6 +13,7 @@ ticket_info = RowTable( 'expiry', 'pardoned_by', 'pardoned_at', 'pardoned_reason'), 'ticketid', + cache_size=20000 ) tickets = Table('tickets')