From d7daa85270fe470c9c47756195315b397af7eb5f Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 11:58:38 +0100 Subject: [PATCH 01/13] refactor(timeout): no default reason arg value in definition This stops the default value showing in `[p]help timeout` --- timeout/timeout.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index fff5fed6..afc2c023 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -267,7 +267,7 @@ async def timeoutset_list(self, ctx: commands.Context): @commands.command() @checks.mod() - async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason="Unspecified"): + async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Timeouts a user or returns them from timeout if they are currently in timeout. See and edit current configuration with `[p]timeoutset`. @@ -309,6 +309,10 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason=" if booster_role in user.roles: timeout_roleset.add(booster_role) + # Assign reason string if not specified by user + if reason is None: + reason = "Unspecified" + # Check if user already in timeout. # Remove & restore if so, else add to timeout. if set(user.roles) == {everyone_role} | timeout_roleset: From f51b5366efbf235ab7d42070f741d0265a2b741b Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 12:01:29 +0100 Subject: [PATCH 02/13] refactor(timeout): more help examples and detail --- timeout/timeout.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index afc2c023..4a827ca1 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -272,8 +272,14 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: See and edit current configuration with `[p]timeoutset`. - Example: + Examples: - `[p]timeout @user` + - `[p]timeout @user Spamming chat` + + If the user is not already in timeout, their roles will be stored, stripped, and replaced with the timeout role. + If the user is already in timeout, they will be removed from the timeout role and have their former roles restored. + + The cog determines that user is currently in timeout if the user's only role is the configured timeout role. """ author = ctx.author everyone_role = ctx.guild.default_role From aff255ae6fad7621a4d3c3b03afe0657cdae5338 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:14:50 +0100 Subject: [PATCH 03/13] feat(timeout): don't allow adding mods+ to timeout --- timeout/timeout.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index 4a827ca1..cae39208 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -3,6 +3,7 @@ import discord from redbot.core import Config, checks, commands +from redbot.core.utils.mod import is_mod_or_superior as is_mod log = logging.getLogger("red.rhomelab.timeout") @@ -297,12 +298,14 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: return # Notify and stop if command author tries to timeout themselves, - # or if the bot can't do that. + # another mod, or if the bot can't do that due to Discord role heirarchy. if author == user: + await ctx.message.add_reaction("🚫") await ctx.send("I cannot let you do that. Self-harm is bad \N{PENSIVE FACE}") return if ctx.guild.me.top_role <= user.top_role or user == ctx.guild.owner: + await ctx.message.add_reaction("🚫") await ctx.send("I cannot do that due to Discord hierarchy rules.") return @@ -315,6 +318,11 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: if booster_role in user.roles: timeout_roleset.add(booster_role) + if await is_mod(ctx.bot, user): + await ctx.message.add_reaction("🚫") + await ctx.send("Nice try. I can't timeout other moderators or admins.") + return + # Assign reason string if not specified by user if reason is None: reason = "Unspecified" From d3d3e382e5d0f23cfc1ee62f7d0857f985c5f6b2 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:28:09 +0100 Subject: [PATCH 04/13] feat(timeout): remove former guild members' config --- timeout/timeout.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/timeout/timeout.py b/timeout/timeout.py index cae39208..a2f00791 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -28,6 +28,19 @@ def __init__(self): # Helper functions + async def member_data_cleanup(self, ctx: commands.Context): + """Remove data stored for members who are no longer in the guild + This helps avoid permanently storing role lists for members who left whilst in timeout. + """ + + member_data = await self.config.all_members(ctx.guild) + + for member in member_data: + # If member not found in guild... + if ctx.guild.get_member(member) is None: + # Clear member data + await self.config.member_from_ids(ctx.guild.id, member).clear() + async def report_handler(self, ctx: commands.Context, user: discord.Member, action_info: dict): """Build and send embed reports""" @@ -334,3 +347,6 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: else: await self.timeout_add(ctx, user, reason, timeout_role, list(timeout_roleset)) + + # Run member data cleanup + await self.member_data_cleanup(ctx) From a2338d7287717e24e0941c802a47a7c7c6384d38 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 15:37:14 +0100 Subject: [PATCH 05/13] refactor(timeout): don't use empty strings for default config --- timeout/timeout.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index a2f00791..b26bc409 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -14,9 +14,9 @@ class Timeout(commands.Cog): def __init__(self): self.config = Config.get_conf(self, identifier=539343858187161140) default_guild = { - "logchannel": "", - "report": "", - "timeoutrole": "" + "logchannel": None, + "report": False, + "timeoutrole": None } self.config.register_guild(**default_guild) self.config.register_member( @@ -244,12 +244,12 @@ async def timeoutset_list(self, ctx: commands.Context): else: log_channel = "Unconfigured" - if timeout_role is not None: + if timeout_role: timeout_role = timeout_role.name else: timeout_role = "Unconfigured" - if report == "": + if not report: report = "Unconfigured" # Build embed From bcc5941279ce76475223aa13fb8c4bf47c7adfdb Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 15:41:59 +0100 Subject: [PATCH 06/13] refactor(timeout): rework report command Improve intuitivity by using enable/disable instead of true/false/yes/no --- timeout/timeout.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index b26bc409..fe8b7d3a 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -194,30 +194,31 @@ async def timeoutset_logchannel(self, ctx: commands.Context, channel: discord.Te await self.config.guild(ctx.guild).logchannel.set(channel.id) await ctx.message.add_reaction("✅") - @timeoutset.command(name="report") + @timeoutset.command(name="report", usage="") @checks.mod() async def timeoutset_report(self, ctx: commands.Context, choice: str): - """Whether to send a report when a user is added or removed from timeout. + """Whether to send a report when a user's timeout status is updated. + + These reports will be sent to the configured log channel as an embed. + The embed will specify the user's details and the moderator who executed the command. - These reports will be sent in the form of an embed with timeout reason to the configured log channel. Set log channel with `[p]timeoutset logchannel`. Example: - - `[p]timeoutset report [choice]` - - Possible choices are: - - `true` or `yes`: Reports will be sent. - - `false` or `no`: Reports will not be sent. + - `[p]timeoutset report enable` + - `[p]timeoutset report disable` """ - if str.lower(choice) in ["true", "yes"]: + if str.lower(choice) == "enable": await self.config.guild(ctx.guild).report.set(True) await ctx.message.add_reaction("✅") - elif str.lower(choice) in ["false", "no"]: + + elif str.lower(choice) == "disable": await self.config.guild(ctx.guild).report.set(False) await ctx.message.add_reaction("✅") + else: - await ctx.send("Choices: true/yes or false/no") + await ctx.send("Setting must be `enable` or `disable`.") @timeoutset.command(name="role") @checks.mod() @@ -249,8 +250,10 @@ async def timeoutset_list(self, ctx: commands.Context): else: timeout_role = "Unconfigured" - if not report: - report = "Unconfigured" + if report: + report = "Enabled" + else: + report = "Disabled" # Build embed embed = discord.Embed( From 694dfd31fd3026ca0fb6f2a0528cb3a8a9d17f1b Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 15:42:59 +0100 Subject: [PATCH 07/13] refactor(timeout): require logchannel before report enable --- timeout/timeout.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index fe8b7d3a..ac056cc5 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -209,9 +209,18 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): - `[p]timeoutset report disable` """ + # Ensure log channel has been defined + log_channel = await self.config.guild(ctx.guild).logchannel() + if str.lower(choice) == "enable": - await self.config.guild(ctx.guild).report.set(True) - await ctx.message.add_reaction("✅") + if log_channel: + await self.config.guild(ctx.guild).report.set(True) + await ctx.message.add_reaction("✅") + else: + await ctx.send( + "You must set the log channel before enabling reports.\n" + + f"Set the log channel with `{ctx.clean_prefix}timeoutset logchannel`." + ) elif str.lower(choice) == "disable": await self.config.guild(ctx.guild).report.set(False) @@ -309,10 +318,6 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: timeout_role_data = await self.config.guild(ctx.guild).timeoutrole() timeout_role = ctx.guild.get_role(timeout_role_data) - if await self.config.guild(ctx.guild).report() and not await self.config.guild(ctx.guild).logchannel(): - await ctx.send("Please set the log channel using `[p]timeoutset logchannel`, or disable reporting.") - return - # Notify and stop if command author tries to timeout themselves, # another mod, or if the bot can't do that due to Discord role heirarchy. if author == user: From 26b9bb1f665f5f32819cf08681ea35d8ae9e1164 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 15:43:46 +0100 Subject: [PATCH 08/13] refactor(timeout): use clean_prefix in message --- timeout/timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index ac056cc5..9bf26573 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -108,7 +108,7 @@ async def timeout_add( await user.edit(roles=timeout_roleset) log.info("User %s added to timeout by %s.", self.target, self.actor) except AttributeError: - await ctx.send("Please set the timeout role using `[p]timeoutset role`.") + await ctx.send(f"Please set the timeout role with `{ctx.clean_prefix}timeoutset role`.") return except discord.Forbidden as error: await ctx.send("Whoops, looks like I don't have permission to do that.") From 45828f94badaef8f9e11df967dd8cf55221f8f54 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 17:24:52 +0100 Subject: [PATCH 09/13] docs: add/fix timeout cog info --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6027b5a5..6beeb537 100644 --- a/README.md +++ b/README.md @@ -363,13 +363,20 @@ This cog exposes a HTTP endpoint for exporting guild metrics in Prometheus forma Manage the timeout status of users. Run the command to add a user to timeout, run it again to remove them. Append a reason if you wish: `[p]timeout @someUser said a bad thing` + If the user is not in timeout, they are added. If they are in timeout, they are removed. + All of the member's roles will be stripped when they are added to timeout, and re-added when they are removed. +This cog is designed for guilds with a private channel used to discuss infractions or otherwise with a given member 1-1. +This private channel should be readable only by mods, admins, and the timeout role. + +**Note:** This cog does not manage Discord's builtin "time out" functionality. It is unrelated. + - `[p]timeout [reason]` - Add/remove a user from timeout, optionally specifying a reason. - `[p]timeoutset list` - Print the current configuration. - `[p]timeoutset role ` - Set the timeout role. -- `[p]timeoutset report ` - Set whether timeout reports should be logged or not. +- `[p]timeoutset report ` - Set whether timeout reports should be logged or not. - `[p]timeoutset logchannel ` - Set the log channel. ### Topic From 982c02cfb9a0e28fe2752db657ee22474ff58c16 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Tue, 11 Oct 2022 17:40:03 +0100 Subject: [PATCH 10/13] style(timeout): import datetime as dt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cause why not ¯\_(ツ)_/¯ --- timeout/timeout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index 9bf26573..2a160671 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -1,5 +1,5 @@ -import datetime import logging +from datetime import datetime as dt import discord from redbot.core import Config, checks, commands @@ -52,7 +52,7 @@ async def report_handler(self, ctx: commands.Context, user: discord.Member, acti embed = discord.Embed( description=f"{user.mention} ({user.id})", color=(await ctx.embed_colour()), - timestamp=datetime.datetime.utcnow() + timestamp=dt.utcnow() ) embed.add_field( name="Moderator", From fcf207a7c3bfb4dd4d816aacece71e539017a667 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:30:02 +0100 Subject: [PATCH 11/13] refactor(timeout): use tick method, not add_reaction --- timeout/timeout.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index 2a160671..3d36f021 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -125,7 +125,7 @@ async def timeout_add( f"Attempted new roles: {timeout_roleset}", exc_info=error ) else: - await ctx.message.add_reaction("✅") + await ctx.tick() # Send report to channel if await self.config.guild(ctx.guild).report(): @@ -161,7 +161,7 @@ async def timeout_remove(self, ctx: commands.Context, user: discord.Member, reas f"Attempted new roles: {user_roles}", exc_info=error ) else: - await ctx.message.add_reaction("✅") + await ctx.tick() # Clear user's roles from config await self.config.member(user).clear() @@ -192,7 +192,7 @@ async def timeoutset_logchannel(self, ctx: commands.Context, channel: discord.Te - `[p]timeoutset logchannel #mod-log` """ await self.config.guild(ctx.guild).logchannel.set(channel.id) - await ctx.message.add_reaction("✅") + await ctx.tick() @timeoutset.command(name="report", usage="") @checks.mod() @@ -215,7 +215,7 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): if str.lower(choice) == "enable": if log_channel: await self.config.guild(ctx.guild).report.set(True) - await ctx.message.add_reaction("✅") + await ctx.tick() else: await ctx.send( "You must set the log channel before enabling reports.\n" + @@ -224,7 +224,7 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): elif str.lower(choice) == "disable": await self.config.guild(ctx.guild).report.set(False) - await ctx.message.add_reaction("✅") + await ctx.tick() else: await ctx.send("Setting must be `enable` or `disable`.") @@ -238,7 +238,7 @@ async def timeoutset_role(self, ctx: commands.Context, role: discord.Role): - `[p]timeoutset role MyRole` """ await self.config.guild(ctx.guild).timeoutrole.set(role.id) - await ctx.message.add_reaction("✅") + await ctx.tick() @timeoutset.command(name="list", aliases=["show", "view", "settings"]) @checks.mod() From 54d6ba92df399506f23681a9fd32accc0edf20e2 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 12 Oct 2022 19:01:29 +0100 Subject: [PATCH 12/13] feat(timeout): query to purge channel on removal --- timeout/timeout.py | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index 3d36f021..fea2a1c0 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -3,7 +3,9 @@ import discord from redbot.core import Config, checks, commands +from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.mod import is_mod_or_superior as is_mod +from redbot.core.utils.predicates import ReactionPredicate log = logging.getLogger("red.rhomelab.timeout") @@ -16,7 +18,8 @@ def __init__(self): default_guild = { "logchannel": None, "report": False, - "timeoutrole": None + "timeoutrole": None, + "timeout_channel": None } self.config.register_guild(**default_guild) self.config.register_member( @@ -137,6 +140,11 @@ async def timeout_add( async def timeout_remove(self, ctx: commands.Context, user: discord.Member, reason: str): """Remove user from timeout""" + + # Retrieve timeout channel + timeout_channel_config = await self.config.guild(ctx.guild).timeout_channel() + timeout_channel = ctx.guild.get_channel(timeout_channel_config) + # Fetch and define user's previous roles. user_roles = [] for role in await self.config.member(user).roles(): @@ -174,6 +182,17 @@ async def timeout_remove(self, ctx: commands.Context, user: discord.Member, reas } await self.report_handler(ctx, user, action_info) + # Ask if user wishes to clear the timeout channel if they've defined one + if timeout_channel: + archive_query = await ctx.send(f"Do you wish to clear the contents of {timeout_channel.mention}?") + start_adding_reactions(archive_query, ReactionPredicate.YES_OR_NO_EMOJIS) + + pred = ReactionPredicate.yes_or_no(archive_query, ctx.author) + await ctx.bot.wait_for("reaction_add", check=pred) + if pred.result is True: + purge = await timeout_channel.purge(bulk=True) + await ctx.send(f"Cleared {len(purge)} messages from {timeout_channel.mention}.") + # Commands @commands.guild_only() @@ -202,7 +221,7 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): These reports will be sent to the configured log channel as an embed. The embed will specify the user's details and the moderator who executed the command. - Set log channel with `[p]timeoutset logchannel`. + Set log channel with `[p]timeoutset logchannel` before enabling reporting. Example: - `[p]timeoutset report enable` @@ -240,6 +259,19 @@ async def timeoutset_role(self, ctx: commands.Context, role: discord.Role): await self.config.guild(ctx.guild).timeoutrole.set(role.id) await ctx.tick() + @timeoutset.command(name="timeoutchannel") + @checks.mod() + async def timeoutset_timeout_channel(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the timeout channel. + + This is required if you wish to optionaly purge the channel upon removing a user from timeout. + + Example: + - `[p]timeoutset timeoutchannel #timeout` + """ + await self.config.guild(ctx.guild).timeout_channel.set(channel.id) + await ctx.tick() + @timeoutset.command(name="list", aliases=["show", "view", "settings"]) @checks.mod() async def timeoutset_list(self, ctx: commands.Context): @@ -248,6 +280,7 @@ async def timeoutset_list(self, ctx: commands.Context): log_channel = await self.config.guild(ctx.guild).logchannel() report = await self.config.guild(ctx.guild).report() timeout_role = ctx.guild.get_role(await self.config.guild(ctx.guild).timeoutrole()) + timeout_channel = await self.config.guild(ctx.guild).timeout_channel() if log_channel: log_channel = f"<#{log_channel}>" @@ -264,6 +297,11 @@ async def timeoutset_list(self, ctx: commands.Context): else: report = "Disabled" + if timeout_channel: + timeout_channel = f"<#{timeout_channel}>" + else: + timeout_channel = "Unconfigured" + # Build embed embed = discord.Embed( color=(await ctx.embed_colour()) @@ -287,6 +325,11 @@ async def timeoutset_list(self, ctx: commands.Context): value=timeout_role, inline=True ) + embed.add_field( + name="Timeout Channel", + value=timeout_channel, + inline=True + ) # Send embed await ctx.send(embed=embed) From 4eae0c5119c67c391d95c7f280102ced6bfb33d8 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 5 Dec 2024 00:55:46 +0000 Subject: [PATCH 13/13] fix(timeout): resolve type errors albeit somewhat hackily. --- timeout/timeout.py | 67 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/timeout/timeout.py b/timeout/timeout.py index fea2a1c0..d11c76e1 100644 --- a/timeout/timeout.py +++ b/timeout/timeout.py @@ -1,5 +1,6 @@ import logging from datetime import datetime as dt +from typing import Literal import discord from redbot.core import Config, checks, commands @@ -26,8 +27,8 @@ def __init__(self): roles=[] ) - self.actor: str = None - self.target: str = None + self.actor: str | None = None + self.target: str | None = None # Helper functions @@ -35,6 +36,9 @@ async def member_data_cleanup(self, ctx: commands.Context): """Remove data stored for members who are no longer in the guild This helps avoid permanently storing role lists for members who left whilst in timeout. """ + # This should never happen due to checks in other functions, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") member_data = await self.config.all_members(ctx.guild) @@ -47,10 +51,19 @@ async def member_data_cleanup(self, ctx: commands.Context): async def report_handler(self, ctx: commands.Context, user: discord.Member, action_info: dict): """Build and send embed reports""" + # This should never happen due to checks in other functions, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + # Retrieve log channel log_channel_config = await self.config.guild(ctx.guild).logchannel() log_channel = ctx.guild.get_channel(log_channel_config) + # Again, this shouldn't happen due to checks in `timeoutset_logchannel`, but it doesn't hurt to have it and it satisfies type checkers. + if not isinstance(log_channel, discord.TextChannel): + await ctx.send(f"The configured log channel ({log_channel_config}) was not found or is not a text channel.") + return + # Build embed embed = discord.Embed( description=f"{user.mention} ({user.id})", @@ -87,6 +100,11 @@ async def timeout_add( timeout_role: discord.Role, timeout_roleset: list[discord.Role]): """Retrieve and save user's roles, then add user to timeout""" + + # This should never happen due to checks in other functions, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + # Catch users already holding timeout role. # This could be caused by an error in this cog's logic or, # more likely, someone manually adding the user to the role. @@ -141,10 +159,19 @@ async def timeout_add( async def timeout_remove(self, ctx: commands.Context, user: discord.Member, reason: str): """Remove user from timeout""" + # This should never happen due to checks in other functions, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + # Retrieve timeout channel timeout_channel_config = await self.config.guild(ctx.guild).timeout_channel() timeout_channel = ctx.guild.get_channel(timeout_channel_config) + # Again, this shouldn't happen due to checks in `timeoutset_timeout_channel`, but it doesn't hurt to have it and it satisfies type checkers. + if not isinstance(timeout_channel, discord.TextChannel): + await ctx.send(f"The configured log channel ({timeout_channel_config}) was not found or is not a text channel.") + return + # Fetch and define user's previous roles. user_roles = [] for role in await self.config.member(user).roles(): @@ -210,12 +237,16 @@ async def timeoutset_logchannel(self, ctx: commands.Context, channel: discord.Te Example: - `[p]timeoutset logchannel #mod-log` """ + # This should never happen due to the guild_only decorator, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + await self.config.guild(ctx.guild).logchannel.set(channel.id) await ctx.tick() @timeoutset.command(name="report", usage="") @checks.mod() - async def timeoutset_report(self, ctx: commands.Context, choice: str): + async def timeoutset_report(self, ctx: commands.Context, choice: Literal['enable', 'disable']): """Whether to send a report when a user's timeout status is updated. These reports will be sent to the configured log channel as an embed. @@ -228,10 +259,14 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): - `[p]timeoutset report disable` """ + # This should never happen due to the guild_only decorator, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + # Ensure log channel has been defined log_channel = await self.config.guild(ctx.guild).logchannel() - if str.lower(choice) == "enable": + if choice.lower() == "enable": if log_channel: await self.config.guild(ctx.guild).report.set(True) await ctx.tick() @@ -241,7 +276,7 @@ async def timeoutset_report(self, ctx: commands.Context, choice: str): f"Set the log channel with `{ctx.clean_prefix}timeoutset logchannel`." ) - elif str.lower(choice) == "disable": + elif choice.lower() == "disable": await self.config.guild(ctx.guild).report.set(False) await ctx.tick() @@ -256,6 +291,10 @@ async def timeoutset_role(self, ctx: commands.Context, role: discord.Role): Example: - `[p]timeoutset role MyRole` """ + # This should never happen due to the guild_only decorator, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + await self.config.guild(ctx.guild).timeoutrole.set(role.id) await ctx.tick() @@ -269,6 +308,10 @@ async def timeoutset_timeout_channel(self, ctx: commands.Context, channel: disco Example: - `[p]timeoutset timeoutchannel #timeout` """ + # This should never happen due to the guild_only decorator, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") + await self.config.guild(ctx.guild).timeout_channel.set(channel.id) await ctx.tick() @@ -276,6 +319,9 @@ async def timeoutset_timeout_channel(self, ctx: commands.Context, channel: disco @checks.mod() async def timeoutset_list(self, ctx: commands.Context): """Show current settings.""" + # This should never happen due to the guild_only decorator, but it doesn't hurt to have it and it satisfies type checkers. + if ctx.guild is None: + raise TypeError("ctx.guild is None") log_channel = await self.config.guild(ctx.guild).logchannel() report = await self.config.guild(ctx.guild).report() @@ -336,7 +382,7 @@ async def timeoutset_list(self, ctx: commands.Context): @commands.command() @checks.mod() - async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): + async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: str | None = None): """Timeouts a user or returns them from timeout if they are currently in timeout. See and edit current configuration with `[p]timeoutset`. @@ -350,6 +396,11 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: The cog determines that user is currently in timeout if the user's only role is the configured timeout role. """ + # This isn't strictly necessary given the `@commands.guild_only()` decorator, + # but type checks still fail without this condition due to some wonky typing in discord.py + if ctx.guild is None: + raise TypeError("ctx.guild is None") + author = ctx.author everyone_role = ctx.guild.default_role @@ -361,6 +412,10 @@ async def timeout(self, ctx: commands.Context, user: discord.Member, *, reason: timeout_role_data = await self.config.guild(ctx.guild).timeoutrole() timeout_role = ctx.guild.get_role(timeout_role_data) + if timeout_role is None: + await ctx.send(f"Timeout role not found. Please set the timeout role using `{ctx.clean_prefix}timeoutset role`.") + return + # Notify and stop if command author tries to timeout themselves, # another mod, or if the bot can't do that due to Discord role heirarchy. if author == user: