From 558c090583c81c607bc94d22bf8bb923fc12db43 Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:10:29 +0530 Subject: [PATCH 1/7] Defer response in `/force_dump` --- main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index dff2bb2..c9bb432 100644 --- a/main.py +++ b/main.py @@ -606,9 +606,11 @@ async def disconnect(interaction: discord.Interaction): @bot.tree.command(name='force_dump', description='Forcibly dumps configuration data') @app_commands.default_permissions(ban_members=True) async def force_dump(interaction: discord.Interaction): + await interaction.response.defer() bot._busy = 0 await bot.do_busy_work() - await interaction.response.send_message('Configuration data successfully dumped.') + emb = discord.Embed(description=f'✅ Configuration data successfully dumped.') + await interaction.followup.send(embed=emb) @bot.tree.command(name='prune', description='(DANGER) Deletes data of users who are no longer in the server') From 2763a6c55a6202ba3939113fcfadc74d87746d0b Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:13:38 +0530 Subject: [PATCH 2/7] Replace checks.has_perms with default_perms --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index c9bb432..68402d7 100644 --- a/main.py +++ b/main.py @@ -418,7 +418,7 @@ async def setup_hook(self) -> None: @bot.tree.command(name='sync', description='Syncs the slash commands to the bot') -@app_commands.checks.has_permissions(administrator=True, ban_members=True) +@app_commands.default_permissions(administrator=True, ban_members=True) async def sync(interaction: discord.Interaction): """Sync all the slash commands to the bot""" if not interaction.user.guild_permissions.ban_members: @@ -431,7 +431,7 @@ async def sync(interaction: discord.Interaction): @bot.tree.command(name='set_channel', description='Sets the channel to count in') @app_commands.describe(channel='The channel to count in') -@app_commands.checks.has_permissions(ban_members=True) +@app_commands.default_permissions(ban_members=True) async def set_channel(interaction: discord.Interaction, channel: discord.TextChannel): """Command to set the channel to count in""" if not interaction.user.guild_permissions.ban_members: From 045811ba9d118543d2d630a6f863f64e5af0d05c Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:19:09 +0530 Subject: [PATCH 3/7] Show admin commands to admins only Also: added an option to request the `/list_commands` output to be ephemeral --- main.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 68402d7..def6ac7 100644 --- a/main.py +++ b/main.py @@ -444,24 +444,31 @@ async def set_channel(interaction: discord.Interaction, channel: discord.TextCha await interaction.response.send_message(f'Counting channel was set to {channel.mention}') -@bot.tree.command(name='listcmds', description='Lists commands') -async def list_commands(interaction: discord.Interaction): +@bot.tree.command(name='list_commands', description='Lists commands') +@app_commands.describe(ephemeral='Whether the output should be ephemeral') +async def list_commands(interaction: discord.Interaction, ephemeral: bool = True): """Command to list all the slash commands""" emb = discord.Embed(title='Slash Commands', color=discord.Color.blue(), description=''' -**sync** - Syncs the slash commands to the bot (Admins only) -**set_channel** - Sets the channel to count in (Admins only) **listcmds** - Lists all the slash commands **stats_user** - Shows the stats of a specific user **stats_server** - Shows the stats of the server -**leaderboard** - Shows the leaderboard of the server -**set_failed_role** - Sets the role to give when a user fails (Admins only) -**set_reliable_role** - Sets the role to give when a user passes the score of 100 (Admins only) -**remove_failed_role** - Removes the role to give when a user fails (Admins only) -**remove_reliable_role** - Removes the role to give when a user passes the score of 100 (Admins only) -**force_dump** - Forcibly dump bot config data. Use only when no one is actively playing. (Admins only) -**prune** - Remove data for users who are no longer in the server. (Admins only)''') - await interaction.response.send_message(embed=emb) +**leaderboard** - Shows the leaderboard of the server''') + + if interaction.user.guild_permissions.ban_members: + emb.description += '''\n +__Restricted commands__ (Admin-only) +**sync** - Syncs the slash commands to the bot +**set_channel** - Sets the channel to count in +**set_failed_role** - Sets the role to give when a user fails +**set_reliable_role** - Sets the role to give when a user passes the score of 100 +**remove_failed_role** - Removes the role to give when a user fails +**remove_reliable_role** - Removes the role to give when a user passes the score of 100 +**force_dump** - Forcibly dump bot config data. Use only when no one is actively playing. +**prune** - Remove data for users who are no longer in the server. +''' + + await interaction.response.send_message(embed=emb, ephemeral=ephemeral) @bot.tree.command(name='stats_user', description='Shows the user stats') From 66b7afd5e0adacab76dd2385a36409ce050b12b8 Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:51:37 +0530 Subject: [PATCH 4/7] Added `/calc` command --- main.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index def6ac7..341d8fd 100644 --- a/main.py +++ b/main.py @@ -450,7 +450,7 @@ async def list_commands(interaction: discord.Interaction, ephemeral: bool = True """Command to list all the slash commands""" emb = discord.Embed(title='Slash Commands', color=discord.Color.blue(), description=''' -**listcmds** - Lists all the slash commands +**list_commands** - Lists all the slash commands **stats_user** - Shows the stats of a specific user **stats_server** - Shows the stats of the server **leaderboard** - Shows the leaderboard of the server''') @@ -461,9 +461,9 @@ async def list_commands(interaction: discord.Interaction, ephemeral: bool = True **sync** - Syncs the slash commands to the bot **set_channel** - Sets the channel to count in **set_failed_role** - Sets the role to give when a user fails -**set_reliable_role** - Sets the role to give when a user passes the score of 100 -**remove_failed_role** - Removes the role to give when a user fails -**remove_reliable_role** - Removes the role to give when a user passes the score of 100 +**set_reliable_role** - Sets the reliable role +**remove_failed_role** - Unsets the role to give when a user fails +**remove_reliable_role** - Unsets the reliable role **force_dump** - Forcibly dump bot config data. Use only when no one is actively playing. **prune** - Remove data for users who are no longer in the server. ''' @@ -653,5 +653,34 @@ async def prune(interaction: discord.Interaction): conn.close() +@bot.tree.command(name='calc', description='Evaluate a mathematical expression') +@app_commands.describe(expression='The mathematical expression to be evaluated') +async def calc(interaction: discord.Interaction, expression: str) -> None: + await interaction.response.defer() + + emb: discord.Embed = discord.Embed(description='') + + if not all(c in POSSIBLE_CHARACTERS for c in expression) or not any(char.isdigit() for char in expression): + emb.description = f'**Expression:** `{expression}`\n\n❌ Invalid mathematical expression!' + emb.colour = discord.Colour.brand_red() + await interaction.followup.send(embed=emb) + return + + try: + number: int = round(eval(expression)) + emb.description = f'**Expression:** `{expression}`\n\n**Result:** `{number}`' + emb.colour = discord.Colour.brand_green() + await interaction.followup.send(embed=emb) + except SyntaxError: + emb.description = f'**Expression:** `{expression}`\n\n❌ Invalid mathematical expression!' + emb.colour = discord.Colour.brand_red() + await interaction.followup.send(embed=emb) + return + except ZeroDivisionError: + emb.description = f'**Expression:** `{expression}`\n\n❌ Division by zero!' + emb.colour = discord.Colour.brand_red() + await interaction.followup.send(embed=emb) + return + if __name__ == '__main__': bot.run(TOKEN) From 6f8d7193157e5765d65f70249f03b524c73f43a1 Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Tue, 16 Apr 2024 01:22:04 +0530 Subject: [PATCH 5/7] Handle SyntaxError and ZeroDivisionError - Show a message if the message has a syntax error, do not break the chain. - Treat ZeroDivisionError like wrong number and handle accordingly. --- main.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index 341d8fd..4c61f69 100644 --- a/main.py +++ b/main.py @@ -247,12 +247,16 @@ async def on_message(self, message: discord.Message) -> None: if not all(c in POSSIBLE_CHARACTERS for c in content) or not any(char.isdigit() for char in content): return + zero_division: bool = False + try: number: int = round(eval(content)) except SyntaxError: + await message.add_reaction('⚠️') + await message.channel.send(f'Syntax error in mathematical expression!\nThe chain has **not** been broken.') return except ZeroDivisionError: - return + zero_division = True self._busy += 1 @@ -273,40 +277,39 @@ async def on_message(self, message: discord.Message) -> None: else: highest_valid_count = stats[0] - # -------------- - # Wrong number - # -------------- - if int(number) != int(self._config.current_count) + 1: + # ------------- + # Wrong member + # ------------- + if zero_division or (self._config.current_count and self._config.current_member_id == message.author.id): if self.failed_role: self._config.failed_member_id = message.author.id # Designate current user as failed member # Adding/removing failed role is done when not busy - await self.handle_wrong_count(message) + await self.handle_wrong_member(message) c.execute('UPDATE members SET score = score - 1, wrong = wrong + 1 WHERE member_id = ?', (message.author.id,)) - conn.commit() conn.close() await self.schedule_busy_work() - return - # ------------- - # Wrong member - # ------------- - if self._config.current_count and self._config.current_member_id == message.author.id: + # -------------- + # Wrong number + # -------------- + if int(number) != int(self._config.current_count) + 1: if self.failed_role: self._config.failed_member_id = message.author.id # Designate current user as failed member # Adding/removing failed role is done when not busy - await self.handle_wrong_member(message) + await self.handle_wrong_count(message) c.execute('UPDATE members SET score = score - 1, wrong = wrong + 1 WHERE member_id = ?', (message.author.id,)) + conn.commit() conn.close() @@ -616,7 +619,7 @@ async def force_dump(interaction: discord.Interaction): await interaction.response.defer() bot._busy = 0 await bot.do_busy_work() - emb = discord.Embed(description=f'✅ Configuration data successfully dumped.') + emb = discord.Embed(description=f'✅ Configuration data successfully dumped.', colour=discord.Colour.og_blurple()) await interaction.followup.send(embed=emb) From a38bae9a62f34d775e05a54ed782d42d103d7a53 Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:55:15 +0530 Subject: [PATCH 6/7] Modified message in `on_ready()` and `/disconnect` - The message from `on_ready()` will now be an embed instead of a normal message. - Handle cases when the counting channel has been removed or the `current_member` has left the guild. - Response from `/disconnect` will also be an embed. - Embed will be sent via the interaction rather than via the counting channel. --- main.py | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index 4c61f69..4fb7df3 100644 --- a/main.py +++ b/main.py @@ -118,19 +118,44 @@ async def on_ready(self) -> None: """Override the on_ready method""" print(f'Bot is ready as {self.user.name}#{self.user.discriminator}') - if self._config.channel_id is not None: - channel = bot.get_channel(self._config.channel_id) - - if self._config.current_member_id is not None: - member = await channel.guild.fetch_member(self._config.current_member_id) - await channel.send( - f'I\'m now online! Last counted by {member.mention}. The **next** number is ' - f'**{self._config.current_count + 1}**.') - else: - await channel.send(f'I\'m now online!') + busy_work_necessary: bool = False + + if self._config.channel_id: + + channel: Optional[discord.TextChannel] = bot.get_channel(self._config.channel_id) + if channel: # It is possible that the channel was removed, so check if channel exists + + emb: discord.Embed = discord.Embed(description=':green_circle: **I\'m now online!**', + colour=discord.Color.brand_green()) + + if self._config.high_score > 0: + emb.description += (f'\n\n:fire: Let\'s beat the high score of {self._config.high_score}! ' + f':muscle:\n') + + emb.add_field(name='NEXT number', value=f'{self._config.current_count + 1}', inline=True) + + if self._config.current_member_id: + + member: Optional[discord.Member] = channel.guild.get_member(self._config.current_member_id) + if member: # It is possible that the member has left the server, so check if member exists + emb.add_field(name='Last input by', value=f'{member.mention}', inline=True) + + else: # Member has left the server. + self._config.current_member_id = None + emb.add_field(name='Last input by', value=f'An ex-member', inline=True) + busy_work_necessary = True + + await channel.send(embed=emb) + + else: # Counting channel doesn't exist. + self._config.channel_id = None + busy_work_necessary = True self.set_roles() + if busy_work_necessary: + await self.do_busy_work() + def set_roles(self): """ Sets the `self.failed_role` and `self.reliable_counter_role` variables. @@ -606,10 +631,9 @@ async def remove_reliable_role(interaction: discord.Interaction): @bot.tree.command(name='disconnect', description='Makes the bot go offline') @app_commands.default_permissions(ban_members=True) async def disconnect(interaction: discord.Interaction): - config = Config.read() - if config.channel_id is not None: - channel = bot.get_channel(config.channel_id) - await channel.send('Bot is now offline.') + emb: discord.Embed = discord.Embed(description=':octagonal_sign: The bot is going offline :octagonal_sign:', + colour=discord.Colour.brand_red()) + await interaction.response.send_message(embed=emb) await bot.close() From 40a5da7b2b9dd3f6918ef591cb6e847a6a3d9e73 Mon Sep 17 00:00:00 2001 From: Wrichik Basu <56736644+WrichikBasu@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:05:57 +0530 Subject: [PATCH 7/7] Implement `response.defer()` in all slash commands --- main.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 4fb7df3..57e5f78 100644 --- a/main.py +++ b/main.py @@ -465,11 +465,12 @@ async def set_channel(interaction: discord.Interaction, channel: discord.TextCha if not interaction.user.guild_permissions.ban_members: await interaction.response.send_message('You do not have permission to do this!') return + await interaction.response.defer() config = Config.read() config.channel_id = channel.id config.dump_data() bot.read_config() # Explicitly ask the bot to re-read the config - await interaction.response.send_message(f'Counting channel was set to {channel.mention}') + await interaction.followup.send(f'Counting channel was set to {channel.mention}') @bot.tree.command(name='list_commands', description='Lists commands') @@ -538,28 +539,30 @@ async def stats_user(interaction: discord.Interaction, member: discord.Member = @bot.tree.command(name="stats_server", description="View server counting stats") async def stats_server(interaction: discord.Interaction): """Command to show the stats of the server""" + await interaction.response.defer() + # Use the bot's config variable, do not re-read file as it may not have been updated yet config: Config = bot._config if config.channel_id is None: # channel not set yet - await interaction.response.send_message("Counting channel not set yet!") + await interaction.followup.send("Counting channel not set yet!") return - server_stats_embed = discord.Embed( - description=f'''**Current Count**: {config.current_count} + server_stats_embed = discord.Embed(description=f'''**Current Count**: {config.current_count} High Score: {config.high_score} {f"Last counted by: <@{config.current_member_id}>" if config.current_member_id else ""}''', color=discord.Color.blurple() ) server_stats_embed.set_author(name=interaction.guild, icon_url=interaction.guild.icon) - await interaction.response.send_message(embed=server_stats_embed) + await interaction.followup.send(embed=server_stats_embed) @bot.tree.command(name='leaderboard', description='Shows the first 10 users with the highest score') async def leaderboard(interaction: discord.Interaction): """Command to show the top 10 users with the highest score in Indently""" await interaction.response.defer() + emb = discord.Embed(title='Top 10 users in Indently', color=discord.Color.blue(), description='') @@ -582,12 +585,13 @@ async def leaderboard(interaction: discord.Interaction): @app_commands.default_permissions(ban_members=True) async def set_failed_role(interaction: discord.Interaction, role: discord.Role): """Command to set the role to be used when a user fails to count""" + await interaction.response.defer() config = Config.read() config.failed_role_id = role.id config.dump_data() bot.read_config() # Explicitly ask the bot to re-read the config bot.set_roles() # Ask the bot to re-load the roles - await interaction.response.send_message(f'Failed role was set to {role.mention}') + await interaction.followup.send(f'Failed role was set to {role.mention}.') @bot.tree.command(name='set_reliable_role', @@ -596,17 +600,19 @@ async def set_failed_role(interaction: discord.Interaction, role: discord.Role): @app_commands.default_permissions(ban_members=True) async def set_reliable_role(interaction: discord.Interaction, role: discord.Role): """Command to set the role to be used when a user gets 100 of score""" + await interaction.response.defer() config = Config.read() config.reliable_counter_role_id = role.id config.dump_data() bot.read_config() # Explicitly ask the bot to re-read the config bot.set_roles() # Ask the bot to re-load the roles - await interaction.response.send_message(f'Reliable role was set to {role.mention}') + await interaction.followup.send(f'Reliable role was set to {role.mention}.') @bot.tree.command(name='remove_failed_role', description='Removes the failed role feature') @app_commands.default_permissions(ban_members=True) async def remove_failed_role(interaction: discord.Interaction): + await interaction.response.defer() config = Config.read() config.failed_role_id = None config.failed_member_id = None @@ -614,18 +620,19 @@ async def remove_failed_role(interaction: discord.Interaction): config.dump_data() bot.read_config() # Explicitly ask the bot to re-read the config bot.set_roles() # Ask the bot to re-load the roles - await interaction.response.send_message('Failed role removed') + await interaction.followup.send('Failed role removed.') @bot.tree.command(name='remove_reliable_role', description='Removes the reliable role feature') @app_commands.default_permissions(ban_members=True) async def remove_reliable_role(interaction: discord.Interaction): + await interaction.response.defer() config = Config.read() config.reliable_counter_role_id = None config.dump_data() bot.read_config() # Explicitly ask the bot to re-read the config bot.set_roles() # Ask the bot to re-load the roles - await interaction.response.send_message('Reliable role removed') + await interaction.followup.send('Reliable role removed.') @bot.tree.command(name='disconnect', description='Makes the bot go offline') @@ -650,6 +657,7 @@ async def force_dump(interaction: discord.Interaction): @bot.tree.command(name='prune', description='(DANGER) Deletes data of users who are no longer in the server') @app_commands.default_permissions(ban_members=True) async def prune(interaction: discord.Interaction): + await interaction.response.defer() conn: sqlite3.Connection = sqlite3.connect('database.sqlite3') cursor: sqlite3.Cursor = conn.cursor() @@ -670,12 +678,12 @@ async def prune(interaction: discord.Interaction): if count > 0: conn.commit() - await interaction.response.send_message(f'Successfully removed data for {count} user(s).') + await interaction.followup.send(f'Successfully removed data for {count} user(s).') else: - await interaction.response.send_message('No users met the criteria to be removed.') + await interaction.followup.send('No users met the criteria to be removed.') else: - await interaction.response.send_message('No users found in the database.') + await interaction.followup.send('No users found in the database.') conn.close()