diff --git a/cogs/command_error.py b/cogs/command_error.py index fcac8ee12..8d6c08190 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -75,10 +75,11 @@ async def on_application_command_error( command_name: str = ( ctx.command.callback.__name__ if ( - hasattr(ctx.command, "callback") + ctx.command + and hasattr(ctx.command, "callback") and not ctx.command.callback.__name__.startswith("_") ) - else ctx.command.qualified_name + else (ctx.command.qualified_name if ctx.command else "unknown") ) logger.critical( " ".join( diff --git a/cogs/induct.py b/cogs/induct.py index e718fd7d9..2c7af6b9c 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -252,7 +252,9 @@ async def _perform_induction( applicant_role, reason=INDUCT_AUDIT_MESSAGE ) - tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) + tex_emoji: discord.GuildEmoji | discord.AppEmoji | None = self.bot.get_emoji( + 743218410409820213 + ) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") diff --git a/cogs/kill.py b/cogs/kill.py index 2ca4e3b37..7eb149f38 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -25,19 +25,19 @@ class ConfirmKillView(View): """A discord.View containing two buttons to confirm shutting down TeX-Bot.""" - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="SHUTDOWN", style=discord.ButtonStyle.red, custom_id="shutdown_confirm" ) - async def confirm_shutdown_button_callback( # type: ignore[misc] + async def confirm_shutdown_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """When the shutdown button is pressed, delete the message.""" logger.debug('"Confirm" button pressed. %s', interaction) - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="CANCEL", style=discord.ButtonStyle.grey, custom_id="shutdown_cancel" ) - async def cancel_shutdown_button_callback( # type: ignore[misc] + async def cancel_shutdown_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """When the cancel button is pressed, delete the message.""" diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 27e4e1681..7b322bbb3 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -76,7 +76,9 @@ async def _perform_make_applicant( await applicant_member.add_roles(applicant_role, reason=AUDIT_MESSAGE) logger.debug("Applicant role given to user %s", applicant_member) - tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) + tex_emoji: discord.GuildEmoji | discord.AppEmoji | None = self.bot.get_emoji( + 743218410409820213 + ) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") diff --git a/cogs/remind_me.py b/cogs/remind_me.py index bd3457a26..ce99d0e17 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -213,6 +213,11 @@ async def remind_me( parsed_time: tuple[time.struct_time, int] = parsedatetime.Calendar().parseDT( delay, tzinfo=timezone.get_current_timezone() ) + if not ctx.channel: + await self.command_send_error( + ctx, message="This command can only be used in channels." + ) + return if parsed_time[1] == 0: await self.command_send_error( @@ -305,8 +310,8 @@ async def clear_reminders_backlog(self) -> None: discord.utils.utcnow() - reminder.send_datetime ) if time_since_reminder_needed_to_be_sent > datetime.timedelta(minutes=15): - user: discord.User | None = await self.bot.get_or_fetch_user( - int(reminder.discord_member.discord_id) + user: discord.User | None = await self.bot.get_or_fetch( + object_type=discord.User, object_id=int(reminder.discord_member.discord_id) ) if not user: diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 67480680d..b3dbe079d 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -227,13 +227,13 @@ async def send_error( logging_message=logging_message, ) - @ui.button( + @ui.button( # type: ignore[arg-type] label="Opt-out of introduction reminders", custom_id="opt_out_introduction_reminders_button", style=discord.ButtonStyle.red, emoji=discord.PartialEmoji.from_str(emoji.emojize(":no_good:", language="alias")), ) - async def opt_out_introduction_reminders_button_callback( # type: ignore[misc] + async def opt_out_introduction_reminders_button_callback( self, button: discord.Button, interaction: discord.Interaction ) -> None: """ diff --git a/cogs/stats/__init__.py b/cogs/stats/__init__.py index 8b331c222..c8a608b81 100644 --- a/cogs/stats/__init__.py +++ b/cogs/stats/__init__.py @@ -86,7 +86,15 @@ async def channel_stats( # NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent main_guild: discord.Guild = self.bot.main_guild - channel_id: int = ctx.channel_id + if not ctx.channel or isinstance( + ctx.channel, (discord.CategoryChannel, discord.ForumChannel) + ): + await self.command_send_error( + ctx, message="This command can only be used in text channels." + ) + return + + channel_id: int | None = ctx.channel_id if str_channel_id: if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id): @@ -157,6 +165,14 @@ async def server_stats(self, ctx: "TeXBotApplicationContext") -> None: main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role + if not ctx.channel or isinstance( + ctx.channel, (discord.CategoryChannel, discord.ForumChannel) + ): + await self.command_send_error( + ctx, message="This command can only be used in text channels." + ) + return + await ctx.defer(ephemeral=True) message_counts: Mapping[str, Mapping[str, int]] = await get_server_message_counts( @@ -237,6 +253,14 @@ async def user_stats(self, ctx: "TeXBotApplicationContext") -> None: interaction_member: discord.Member = await self.bot.get_main_guild_member(ctx.user) guest_role: discord.Role = await self.bot.guest_role + if not ctx.channel or isinstance( + ctx.channel, (discord.CategoryChannel, discord.ForumChannel) + ): + await self.command_send_error( + ctx, message="This command can only be used in text channels." + ) + return + if guest_role not in interaction_member.roles: await self.command_send_error( ctx, @@ -314,6 +338,14 @@ async def left_member_stats(self, ctx: "TeXBotApplicationContext") -> None: # NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent main_guild: discord.Guild = self.bot.main_guild + if not ctx.channel or isinstance( + ctx.channel, (discord.CategoryChannel, discord.ForumChannel) + ): + await self.command_send_error( + ctx, message="This command can only be used in text channels." + ) + return + await ctx.defer(ephemeral=True) left_member_counts: dict[str, int] = { diff --git a/cogs/strike.py b/cogs/strike.py index 0f3778f72..5961983e4 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -91,10 +91,10 @@ async def perform_moderation_action( class ConfirmStrikeMemberView(View): """A discord.View containing two buttons to confirm giving the member a strike.""" - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="Yes", style=discord.ButtonStyle.red, custom_id="yes_strike_member" ) - async def yes_strike_member_button_callback( # type: ignore[misc] + async def yes_strike_member_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -110,10 +110,10 @@ async def yes_strike_member_button_callback( # type: ignore[misc] view=None ) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="No", style=discord.ButtonStyle.grey, custom_id="no_strike_member" ) - async def no_strike_member_button_callback( # type: ignore[misc] + async def no_strike_member_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -133,10 +133,10 @@ async def no_strike_member_button_callback( # type: ignore[misc] class ConfirmManualModerationView(View): """A discord.View to confirm manually applying a moderation action.""" - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="Yes", style=discord.ButtonStyle.red, custom_id="yes_manual_moderation_action" ) - async def yes_manual_moderation_action_button_callback( # type: ignore[misc] + async def yes_manual_moderation_action_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -153,10 +153,10 @@ async def yes_manual_moderation_action_button_callback( # type: ignore[misc] view=None ) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="No", style=discord.ButtonStyle.grey, custom_id="no_manual_moderation_action" ) - async def no_manual_moderation_action_button_callback( # type: ignore[misc] + async def no_manual_moderation_action_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -177,10 +177,10 @@ async def no_manual_moderation_action_button_callback( # type: ignore[misc] class ConfirmStrikesOutOfSyncWithBanView(View): """A discord.View containing two buttons to confirm banning a member with > 3 strikes.""" - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="Yes", style=discord.ButtonStyle.red, custom_id="yes_out_of_sync_ban_member" ) - async def yes_out_of_sync_ban_member_button_callback( # type: ignore[misc] + async def yes_out_of_sync_ban_member_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -197,10 +197,10 @@ async def yes_out_of_sync_ban_member_button_callback( # type: ignore[misc] view=None ) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error - @discord.ui.button( + @discord.ui.button( # type: ignore[arg-type] label="No", style=discord.ButtonStyle.grey, custom_id="no_out_of_sync_ban_member" ) - async def no_out_of_sync_ban_member_button_callback( # type: ignore[misc] + async def no_out_of_sync_ban_member_button_callback( self, _: discord.Button, interaction: discord.Interaction ) -> None: """ @@ -264,7 +264,7 @@ async def _send_strike_user_message( async def _confirm_perform_moderation_action( self, message_sender_component: "MessageSavingSenderComponent", - interaction_user: discord.User, + interaction_user: discord.User | discord.Member, strike_user: discord.Member, confirm_strike_message: str, actual_strike_amount: int, @@ -315,7 +315,7 @@ async def _confirm_perform_moderation_action( async def _confirm_increase_strike( self, message_sender_component: "MessageSavingSenderComponent", - interaction_user: discord.User, + interaction_user: discord.User | discord.Member, strike_user: discord.User | discord.Member, member_strikes: DiscordMemberStrikes, button_callback_channel: discord.TextChannel | discord.DMChannel, @@ -401,6 +401,12 @@ async def _command_perform_strike( Also calls the process of performing the appropriate moderation action, given the new number of strikes that the member has. """ + if not isinstance(ctx.channel, (discord.TextChannel, discord.DMChannel)): + await self.command_send_error( + ctx, message="This command can only be used in text channels or DMs." + ) + return + if strike_member.bot: await self.command_send_error( ctx, @@ -504,7 +510,8 @@ async def _confirm_manual_add_strike( # noqa: PLR0915 async for _audit_log_entry in main_guild.audit_logs( after=discord.utils.utcnow() - datetime.timedelta(minutes=1), action=action ) - if _audit_log_entry.target.id + if _audit_log_entry.target + and _audit_log_entry.target.id == strike_user.id # NOTE: IDs are checked here rather than the objects themselves as the audit log provides an unusual object type in some cases. ) except (StopIteration, StopAsyncIteration): @@ -750,9 +757,13 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) audit_log_entry: discord.AuditLogEntry async for audit_log_entry in main_guild.audit_logs(limit=5): - FOUND_CORRECT_AUDIT_LOG_ENTRY: bool = audit_log_entry.target.id == after.id and ( - audit_log_entry.action - == discord.AuditLogAction.auto_moderation_user_communication_disabled + FOUND_CORRECT_AUDIT_LOG_ENTRY: bool = ( + (audit_log_entry.target is not None) + and (audit_log_entry.target.id == after.id) + and ( + audit_log_entry.action + == discord.AuditLogAction.auto_moderation_user_communication_disabled + ) ) if FOUND_CORRECT_AUDIT_LOG_ENTRY: await self._confirm_manual_add_strike( diff --git a/pyproject.toml b/pyproject.toml index 0888b0ace..7909a0055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ main = [ "matplotlib>=3.10", "mplcyberpunk>=0.7", "parsedatetime>=2.6", - "py-cord>=2.6,<2.7", + "py-cord>=2.8", "python-dotenv>=1.0", "python-logging-discord-handler>=0.1", "typed_classproperties>=1.2", diff --git a/stubs/discord/__init__.pyi b/stubs/discord/__init__.pyi index 83fa70d4d..f1a003ed5 100644 --- a/stubs/discord/__init__.pyi +++ b/stubs/discord/__init__.pyi @@ -62,7 +62,6 @@ from .team import * from .template import * from .threads import * from .user import * -from .voice_client import * from .webhook import * from .welcome_screen import * from .widget import * diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 99de05c11..dd2e2ec58 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -83,7 +83,7 @@ async def delete(self) -> None: await self.sent_message.delete() else: - await self.sent_message.delete_original_message() + await self.sent_message.delete_original_response() class ChannelMessageSender(MessageSavingSenderComponent): diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index e18c06e9a..16d2a71d5 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -84,10 +84,11 @@ async def command_send_error( COMMAND_NAME: Final[str] = ( ctx.command.callback.__name__ if ( - hasattr(ctx.command, "callback") + ctx.command + and hasattr(ctx.command, "callback") and not ctx.command.callback.__name__.startswith("_") ) - else ctx.command.qualified_name + else (ctx.command.qualified_name if ctx.command else "unknown") ) await self.send_error( diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 52a88c506..0701bccf8 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -43,4 +43,4 @@ class TeXBotApplicationContext(discord.ApplicationContext): bot: "TeXBot" # type: ignore[mutable-override] - respond: "Callable[..., Awaitable[Interaction | WebhookMessage]]" # type: ignore[explicit-any] + respond: "Callable[..., Awaitable[Interaction | WebhookMessage]]" # type: ignore[assignment, explicit-any] diff --git a/uv.lock b/uv.lock index 1b968b48f..9d86a5c87 100644 --- a/uv.lock +++ b/uv.lock @@ -818,14 +818,15 @@ wheels = [ [[package]] name = "py-cord" -version = "2.6.1" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/c7/c539d69d5cfa1ea5891d596212f73d619e40c7fc9f02ae906f4147993b94/py_cord-2.6.1.tar.gz", hash = "sha256:36064f225f2c7bbddfe542d5ed581f2a5744f618e039093cf7cd2659a58bc79b", size = 965087, upload-time = "2024-09-15T19:36:39.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/73/02ad1b5ced7685e2868c25d574edc6b4eb154a214cf4caf36270480fc65c/py_cord-2.8.0.tar.gz", hash = "sha256:57a7efefa80e7f6ef890fbfc054d686782c347c9e4d86fa7c91f7ae5285b353f", size = 1173848, upload-time = "2026-05-18T13:02:09.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/90/2690ded84e34b15ca2619932a358c1b7dc6d28fe845dfbd01929fc33c9da/py_cord-2.6.1-py3-none-any.whl", hash = "sha256:e3d3b528c5e37b0e0825f5b884cbb9267860976c1e4878e28b55da8fd3af834b", size = 1089154, upload-time = "2024-09-15T19:36:35.34Z" }, + { url = "https://files.pythonhosted.org/packages/b1/52/cd690a9103b3deadc5c047d3823b750a9afd61c35e57373bbd9e5dbd765f/py_cord-2.8.0-py3-none-any.whl", hash = "sha256:24f9180b4df432ddef732e388a905aa7d9cdf8f2f26318f18a193bccb2d7f642", size = 1264552, upload-time = "2026-05-18T13:02:06.959Z" }, ] [[package]] @@ -1154,7 +1155,7 @@ main = [ { name = "matplotlib", specifier = ">=3.10" }, { name = "mplcyberpunk", specifier = ">=0.7" }, { name = "parsedatetime", specifier = ">=2.6" }, - { name = "py-cord", specifier = ">=2.6,<2.7" }, + { name = "py-cord", specifier = ">=2.8" }, { name = "python-dotenv", specifier = ">=1.0" }, { name = "python-logging-discord-handler", specifier = ">=0.1" }, { name = "typed-classproperties", specifier = ">=1.2" },