diff --git a/cogs/command_error.py b/cogs/command_error.py index fcac8ee12..a6ced1708 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -72,20 +72,28 @@ async def on_application_command_error( if isinstance(error, discord.ApplicationCommandInvokeError) and isinstance( error.original, GuildDoesNotExistError ): - command_name: str = ( + command_name: str | None = ( 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 None) ) logger.critical( " ".join( message_part for message_part in ( error.original.ERROR_CODE, - f"({command_name})" if command_name in self.ERROR_ACTIVITIES else "", + ( + f"({command_name})" + if ( + command_name is not None + and command_name in self.ERROR_ACTIVITIES + ) + else "" + ), str(error.original).rstrip(".:"), ) if message_part diff --git a/cogs/induct.py b/cogs/induct.py index ee18a3b57..e2b8f4226 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -12,6 +12,7 @@ from exceptions import ( ApplicantRoleDoesNotExistError, CommitteeRoleDoesNotExistError, + DiscordMemberNotInMainGuildError, GuestRoleDoesNotExistError, GuildDoesNotExistError, MemberRoleDoesNotExistError, @@ -188,8 +189,8 @@ async def _perform_induction( main_guild: discord.Guild = self.bot.main_guild guest_role: discord.Role = await self.bot.guest_role - induction_user: discord.User | None = await self.bot.get_or_fetch_user( - induction_member_id + induction_user: discord.User | None = await self.bot.get_or_fetch( + discord.User, induction_member_id ) if not induction_user: await ctx.respond( @@ -201,12 +202,15 @@ async def _perform_induction( ) return - induction_member: discord.Member | None = main_guild.get_member(induction_member_id) - if not induction_member: + try: + induction_member: discord.Member = await self.bot.get_main_guild_member( + induction_user + ) + except DiscordMemberNotInMainGuildError: await ctx.respond( ( ":information_source: No changes made. User cannot be inducted " - "because they have left the server or are not cached " + "because they are not in the server " ":information_source:" ), ephemeral=True, @@ -265,7 +269,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..0d3d0e49f 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -28,8 +28,8 @@ class ConfirmKillView(View): @discord.ui.button( label="SHUTDOWN", style=discord.ButtonStyle.red, custom_id="shutdown_confirm" ) - async def confirm_shutdown_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def confirm_shutdown_button_callback( + self, _: discord.ui.Button["ConfirmKillView"], interaction: discord.Interaction ) -> None: """When the shutdown button is pressed, delete the message.""" logger.debug('"Confirm" button pressed. %s', interaction) @@ -37,8 +37,8 @@ async def confirm_shutdown_button_callback( # type: ignore[misc] @discord.ui.button( label="CANCEL", style=discord.ButtonStyle.grey, custom_id="shutdown_cancel" ) - async def cancel_shutdown_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def cancel_shutdown_button_callback( + self, _: discord.ui.Button["ConfirmKillView"], interaction: discord.Interaction ) -> None: """When the cancel button is pressed, delete the message.""" logger.debug('"Cancel" button pressed. %s', interaction) 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/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 67480680d..0ea79421f 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -233,8 +233,12 @@ async def send_error( 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] - self, button: discord.Button, interaction: discord.Interaction + async def opt_out_introduction_reminders_button_callback( + self, + button: discord.ui.Button[ + "SendIntroductionRemindersTaskCog.OptOutIntroductionRemindersView" + ], + interaction: discord.Interaction, ) -> None: """ Set the opt-in/out flag depending on the status of the button. diff --git a/cogs/stats/__init__.py b/cogs/stats/__init__.py index 8b331c222..cf70ad99a 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 cannot be used in category/forum 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 cannot be used in category/forum 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 cannot be used in category/forum 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 cannot be used in category/forum channels." + ) + return + await ctx.defer(ephemeral=True) left_member_counts: dict[str, int] = { diff --git a/cogs/strike.py b/cogs/strike.py index 46fc40ec2..78038c004 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -94,8 +94,8 @@ class ConfirmStrikeMemberView(View): @discord.ui.button( label="Yes", style=discord.ButtonStyle.red, custom_id="yes_strike_member" ) - async def yes_strike_member_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def yes_strike_member_button_callback( + self, _: discord.ui.Button["ConfirmStrikeMemberView"], interaction: discord.Interaction ) -> None: """ Delete the message associated with the view when the Yes button is pressed. @@ -113,8 +113,8 @@ async def yes_strike_member_button_callback( # type: ignore[misc] @discord.ui.button( label="No", style=discord.ButtonStyle.grey, custom_id="no_strike_member" ) - async def no_strike_member_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def no_strike_member_button_callback( + self, _: discord.ui.Button["ConfirmStrikeMemberView"], interaction: discord.Interaction ) -> None: """ Delete the message associated with the view when the No button is pressed. @@ -136,8 +136,10 @@ class ConfirmManualModerationView(View): @discord.ui.button( label="Yes", style=discord.ButtonStyle.red, custom_id="yes_manual_moderation_action" ) - async def yes_manual_moderation_action_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def yes_manual_moderation_action_button_callback( + self, + _: discord.ui.Button["ConfirmManualModerationView"], + interaction: discord.Interaction, ) -> None: """ Delete the message associated with the view when the Yes button is pressed. @@ -156,8 +158,10 @@ async def yes_manual_moderation_action_button_callback( # type: ignore[misc] @discord.ui.button( label="No", style=discord.ButtonStyle.grey, custom_id="no_manual_moderation_action" ) - async def no_manual_moderation_action_button_callback( # type: ignore[misc] - self, _: discord.Button, interaction: discord.Interaction + async def no_manual_moderation_action_button_callback( + self, + _: discord.ui.Button["ConfirmManualModerationView"], + interaction: discord.Interaction, ) -> None: """ Delete the message associated with the view when the No button is pressed. @@ -180,8 +184,10 @@ class ConfirmStrikesOutOfSyncWithBanView(View): @discord.ui.button( 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] - self, _: discord.Button, interaction: discord.Interaction + async def yes_out_of_sync_ban_member_button_callback( + self, + _: discord.ui.Button["ConfirmStrikesOutOfSyncWithBanView"], + interaction: discord.Interaction, ) -> None: """ Delete the message associated with the view when the Yes button is pressed. @@ -200,8 +206,10 @@ async def yes_out_of_sync_ban_member_button_callback( # type: ignore[misc] @discord.ui.button( 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] - self, _: discord.Button, interaction: discord.Interaction + async def no_out_of_sync_ban_member_button_callback( + self, + _: discord.ui.Button["ConfirmStrikesOutOfSyncWithBanView"], + interaction: discord.Interaction, ) -> None: """ Delete the message associated with the view when the No button is pressed. @@ -229,7 +237,7 @@ class BaseStrikeCog(TeXBotBaseCog): SUGGESTED_ACTIONS: "Final[Mapping[int, str]]" = {1: "time-out", 2: "kick", 3: "ban"} # noqa: RUF012 async def _send_strike_user_message( - self, strike_user: discord.User | discord.Member, member_strikes: DiscordMemberStrikes + self, strike_user: discord.abc.Messageable, member_strikes: DiscordMemberStrikes ) -> None: try: await strike_user.send( @@ -264,7 +272,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,14 +323,14 @@ 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, *, perform_action: bool, ) -> None: - if perform_action and isinstance(strike_user, discord.User): + if perform_action and not isinstance(strike_user, discord.Member): STRIKE_USER_TYPE_ERROR_MESSAGE: Final[str] = ( "Cannot perform moderation action on non-guild member." ) @@ -401,6 +409,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, @@ -453,7 +467,9 @@ async def get_confirmation_message_channel( ) from fetch_log_channel_error raw_user: discord.User | None = ( - self.bot.get_user(user.id) if isinstance(user, discord.Member) else user + user + if isinstance(user, discord.User) + else await self.bot.get_or_fetch(discord.User, user.id) ) if not raw_user: raise StrikeTrackingError @@ -504,8 +520,12 @@ 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 - == 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. + 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): logger.debug("Printing 5 most recent audit logs:") @@ -720,7 +740,9 @@ async def _confirm_manual_add_strike( # noqa: PLR0915 return if button_interaction.data["custom_id"] == "yes_manual_moderation_action": # type: ignore[index, typeddict-item] - interaction_user: discord.User | None = self.bot.get_user(applied_action_user.id) + interaction_user: discord.User | None = await self.bot.get_or_fetch( + discord.User, applied_action_user.id + ) if not interaction_user: raise StrikeTrackingError @@ -750,11 +772,14 @@ 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 - ) - if FOUND_CORRECT_AUDIT_LOG_ENTRY: + if ( + audit_log_entry.target + and (audit_log_entry.target.id == after.id) + and ( + audit_log_entry.action + == discord.AuditLogAction.auto_moderation_user_communication_disabled + ) + ): await self._confirm_manual_add_strike( strike_user=after, action=audit_log_entry.action ) diff --git a/pyproject.toml b/pyproject.toml index 213348cf0..886be2062 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ main = [ "matplotlib>=3.10,<3.11", "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.py b/utils/tex_bot.py index f86749fd6..1fdde7ea9 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -457,7 +457,9 @@ async def get_main_guild_member( Raises `DiscordMemberNotInMainGuild` if the user is not in your group's Discord guild. """ - main_guild_member: discord.Member | None = self.main_guild.get_member(user.id) + main_guild_member: discord.Member | None = await self.main_guild.get_or_fetch( + discord.Member, user.id + ) if not main_guild_member: raise DiscordMemberNotInMainGuildError(user_id=user.id) @@ -476,7 +478,7 @@ async def get_member_from_str_id(self, str_member_id: str) -> discord.Member: INVALID_USER_ID_MESSAGE: Final[str] = f"'{str_member_id}' is not a valid user ID." raise ValueError(INVALID_USER_ID_MESSAGE) - user: discord.User | None = await self.get_or_fetch_user(int(str_member_id)) + user: discord.User | None = await self.get_or_fetch(discord.User, int(str_member_id)) if not user: raise ValueError( DiscordMemberNotInMainGuildError(user_id=int(str_member_id)).message diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index f78749f85..78da5165b 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -82,13 +82,14 @@ async def command_send_error( The constructed error message is then sent as the response to the given application command context. """ - COMMAND_NAME: Final[str] = ( + COMMAND_NAME: Final[str | None] = ( 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 None) ) await self.send_error( @@ -105,7 +106,7 @@ async def send_error( cls, bot: "TeXBot", interaction: discord.Interaction, - interaction_name: str, + interaction_name: str | None = None, error_code: str | None = None, message: str | None = None, logging_message: str | BaseException | None = None, @@ -125,7 +126,7 @@ async def send_error( f"{error_code}**\n" ) + construct_error_message - if interaction_name in cls.ERROR_ACTIVITIES: + if interaction_name is not None and interaction_name in cls.ERROR_ACTIVITIES: construct_error_message += ( f" when trying to {cls.ERROR_ACTIVITIES[interaction_name]}" ) diff --git a/utils/tex_bot_contexts.py b/utils/tex_bot_contexts.py index 52a88c506..1bd734ae1 100644 --- a/utils/tex_bot_contexts.py +++ b/utils/tex_bot_contexts.py @@ -10,9 +10,7 @@ import discord if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Sequence - - from discord import Interaction, WebhookMessage + from collections.abc import Sequence from utils.tex_bot import TeXBot @@ -42,5 +40,3 @@ class TeXBotApplicationContext(discord.ApplicationContext): """ bot: "TeXBot" # type: ignore[mutable-override] - - respond: "Callable[..., Awaitable[Interaction | WebhookMessage]]" # type: ignore[explicit-any] diff --git a/uv.lock b/uv.lock index 515402c8e..053298bb3 100644 --- a/uv.lock +++ b/uv.lock @@ -787,14 +787,15 @@ wheels = [ [[package]] name = "py-cord" -version = "2.6.1" +version = "2.8.1" 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/48/99/371cafe39067d46db88477a4badbbf4128938d5a11971f9f0b8f4581efa1/py_cord-2.8.1.tar.gz", hash = "sha256:b1ebd22416941bbeccb6d26df5cbd65ff6b1d47fcbbc9652efae039e46e8a562", size = 1173996, upload-time = "2026-07-25T16:40:10.435Z" } 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/e3/a6/7aeceb7dfa04c0d390b89ea0b4b980963b93cd972ab6306ad9684811dc6c/py_cord-2.8.1-py3-none-any.whl", hash = "sha256:8b97a61cceb4ca9000fc3ac1190fa4c45ef6a521d88707953f7eeadc1bd57071", size = 1264633, upload-time = "2026-07-25T16:40:08.96Z" }, ] [[package]] @@ -1124,7 +1125,7 @@ main = [ { name = "matplotlib", specifier = ">=3.10,<3.11" }, { 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" },