From a0b0a97e5256721e8b994ecafc6a721a8a7f80d0 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 27 Oct 2024 14:49:41 -0400 Subject: [PATCH 1/9] Support enforce_nonce and add random nonce for message creation --- discord/abc.py | 4 ++++ discord/http.py | 1 + 2 files changed, 5 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index af2b15dac79c..891404b33f4a 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -26,6 +26,7 @@ import copy import time +import secrets import asyncio from datetime import datetime from typing import ( @@ -1614,6 +1615,9 @@ async def send( else: flags = MISSING + if nonce is None: + nonce = secrets.randbits(64) + with handle_message_parameters( content=content, tts=tts, diff --git a/discord/http.py b/discord/http.py index fbaf447aa415..c66132055413 100644 --- a/discord/http.py +++ b/discord/http.py @@ -197,6 +197,7 @@ def handle_message_parameters( if nonce is not None: payload['nonce'] = str(nonce) + payload['enforce_nonce'] = True if message_reference is not MISSING: payload['message_reference'] = message_reference From ed615887f073e0dd74de0c43d56a65934582e903 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 28 Oct 2024 18:11:33 -0400 Subject: [PATCH 2/9] Handle improper 1000 closures by Discord --- discord/gateway.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/gateway.py b/discord/gateway.py index 13a213ce3ee9..d15c617d14c0 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -606,7 +606,10 @@ def latency(self) -> float: def _can_handle_close(self) -> bool: code = self._close_code or self.socket.close_code - return code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014) + # If the socket is closed remotely with 1000 and it's not our own explicit close + # then it's an improper close that should be handled and reconnected + is_improper_close = self._close_code is None and self.socket.close_code == 1000 + return is_improper_close or code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014) async def poll_event(self) -> None: """Polls for a DISPATCH event and handles the general gateway loop. From d08fd59434c38a13d621aea458b0459e48be8466 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 30 Oct 2024 08:08:45 -0400 Subject: [PATCH 3/9] Avoid returning in finally Specifically reraise KeyboardInterrupt, SystemExit Swallow other BaseExceptions due to the way the standard library uses them and the intent of this function --- discord/player.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/discord/player.py b/discord/player.py index 5b2c99dc04d4..bad6da88ed92 100644 --- a/discord/player.py +++ b/discord/player.py @@ -588,22 +588,26 @@ async def probe( loop = asyncio.get_running_loop() try: codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) - except Exception: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: if not fallback: _log.exception("Probe '%s' using '%s' failed", method, executable) - return # type: ignore + return None, None _log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable) try: codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) - except Exception: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: _log.exception("Fallback probe using '%s' failed", executable) else: _log.debug("Fallback probe found codec=%s, bitrate=%s", codec, bitrate) else: _log.debug("Probe found codec=%s, bitrate=%s", codec, bitrate) - finally: - return codec, bitrate + + return codec, bitrate @staticmethod def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]: From af75985730528fa76f9949ea768ae90fd2a50c75 Mon Sep 17 00:00:00 2001 From: Steve C Date: Wed, 30 Oct 2024 20:56:37 -0400 Subject: [PATCH 4/9] Fix incorrect import --- discord/channel.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 58fc2524f40b..02dd9024beb1 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -65,7 +65,7 @@ from .stage_instance import StageInstance from .threads import Thread from .partial_emoji import _EmojiTag, PartialEmoji -from .flags import ChannelFlags +from .flags import ChannelFlags, MessageFlags from .http import handle_message_parameters from .object import Object from .soundboard import BaseSoundboardSound, SoundboardDefaultSound @@ -2938,8 +2938,6 @@ async def create_thread( raise TypeError(f'view parameter must be View not {view.__class__.__name__}') if suppress_embeds: - from .message import MessageFlags # circular import - flags = MessageFlags._from_value(4) else: flags = MISSING From c7305b022cc5898bbd27ca03cfc9bd75ce5c11e6 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:07:20 +0100 Subject: [PATCH 5/9] [commands] Respect enabled kwarg for hybrid app commands --- discord/ext/commands/hybrid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index 8c2f9a9e9d65..e84e7e03e9e5 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -43,7 +43,7 @@ from discord import app_commands from discord.utils import MISSING, maybe_coroutine, async_all from .core import Command, Group -from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError +from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError, DisabledCommand from .converter import Converter, Range, Greedy, run_converters, CONVERTER_MAPPING from .parameters import Parameter from .flags import is_flag, FlagConverter @@ -526,6 +526,9 @@ def cog(self, value: CogT) -> None: self.app_command.binding = value async def can_run(self, ctx: Context[BotT], /) -> bool: + if not self.enabled: + raise DisabledCommand(f'{self.name} command is disabled') + if ctx.interaction is not None and self.app_command: return await self.app_command._check_can_run(ctx.interaction) else: From 814ce3c8ee5cd4a823922d7cff16347f2231ce3d Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:19:43 +0100 Subject: [PATCH 6/9] Add command target to MessageInteractionMetadata --- discord/message.py | 40 +++++++++++++++++++++++++++--- discord/types/interactions.py | 46 ++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/discord/message.py b/discord/message.py index 1f2cea3ae9ba..a58921531159 100644 --- a/discord/message.py +++ b/discord/message.py @@ -828,6 +828,14 @@ class MessageInteractionMetadata(Hashable): The ID of the message that containes the interactive components, if applicable. modal_interaction: Optional[:class:`.MessageInteractionMetadata`] The metadata of the modal submit interaction that triggered this interaction, if applicable. + target_user: Optional[:class:`User`] + The user the command was run on, only applicable to user context menus. + + .. versionadded:: 2.5 + target_message_id: Optional[:class:`int`] + The ID of the message the command was run on, only applicable to message context menus. + + .. versionadded:: 2.5 """ __slots__: Tuple[str, ...] = ( @@ -837,6 +845,8 @@ class MessageInteractionMetadata(Hashable): 'original_response_message_id', 'interacted_message_id', 'modal_interaction', + 'target_user', + 'target_message_id', '_integration_owners', '_state', '_guild', @@ -848,31 +858,43 @@ def __init__(self, *, state: ConnectionState, guild: Optional[Guild], data: Mess self.id: int = int(data['id']) self.type: InteractionType = try_enum(InteractionType, data['type']) - self.user = state.create_user(data['user']) + self.user: User = state.create_user(data['user']) self._integration_owners: Dict[int, int] = { int(key): int(value) for key, value in data.get('authorizing_integration_owners', {}).items() } self.original_response_message_id: Optional[int] = None try: - self.original_response_message_id = int(data['original_response_message_id']) + self.original_response_message_id = int(data['original_response_message_id']) # type: ignore # EAFP except KeyError: pass self.interacted_message_id: Optional[int] = None try: - self.interacted_message_id = int(data['interacted_message_id']) + self.interacted_message_id = int(data['interacted_message_id']) # type: ignore # EAFP except KeyError: pass self.modal_interaction: Optional[MessageInteractionMetadata] = None try: self.modal_interaction = MessageInteractionMetadata( - state=state, guild=guild, data=data['triggering_interaction_metadata'] + state=state, guild=guild, data=data['triggering_interaction_metadata'] # type: ignore # EAFP ) except KeyError: pass + self.target_user: Optional[User] = None + try: + self.target_user = state.create_user(data['target_user']) # type: ignore # EAFP + except KeyError: + pass + + self.target_message_id: Optional[int] = None + try: + self.target_message_id = int(data['target_message_id']) # type: ignore # EAFP + except KeyError: + pass + def __repr__(self) -> str: return f'' @@ -899,6 +921,16 @@ def interacted_message(self) -> Optional[Message]: return self._state._get_message(self.interacted_message_id) return None + @property + def target_message(self) -> Optional[Message]: + """Optional[:class:`~discord.Message`]: The target message, if applicable and is found in cache. + + .. versionadded:: 2.5 + """ + if self.target_message_id: + return self._state._get_message(self.target_message_id) + return None + def is_guild_integration(self) -> bool: """:class:`bool`: Returns ``True`` if the interaction is a guild integration.""" if self._guild: diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 7aac5df7d095..a72a5b2cea15 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -255,11 +255,49 @@ class MessageInteraction(TypedDict): member: NotRequired[Member] -class MessageInteractionMetadata(TypedDict): +class _MessageInteractionMetadata(TypedDict): id: Snowflake - type: InteractionType user: User authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake] original_response_message_id: NotRequired[Snowflake] - interacted_message_id: NotRequired[Snowflake] - triggering_interaction_metadata: NotRequired[MessageInteractionMetadata] + + +class _ApplicationCommandMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[2] + # command_type: Literal[1, 2, 3, 4] + + +class UserApplicationCommandMessageInteractionMetadata(_ApplicationCommandMessageInteractionMetadata): + # command_type: Literal[2] + target_user: User + + +class MessageApplicationCommandMessageInteractionMetadata(_ApplicationCommandMessageInteractionMetadata): + # command_type: Literal[3] + target_message_id: Snowflake + + +ApplicationCommandMessageInteractionMetadata = Union[ + _ApplicationCommandMessageInteractionMetadata, + UserApplicationCommandMessageInteractionMetadata, + MessageApplicationCommandMessageInteractionMetadata, +] + + +class MessageComponentMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[3] + interacted_message_id: Snowflake + + +class ModalSubmitMessageInteractionMetadata(_MessageInteractionMetadata): + type: Literal[5] + triggering_interaction_metadata: Union[ + ApplicationCommandMessageInteractionMetadata, MessageComponentMessageInteractionMetadata + ] + + +MessageInteractionMetadata = Union[ + ApplicationCommandMessageInteractionMetadata, + MessageComponentMessageInteractionMetadata, + ModalSubmitMessageInteractionMetadata, +] From 7db879b5bd7008a28613cf107f434a18b8ea7303 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:20:01 +0100 Subject: [PATCH 7/9] Clear up add_roles and remove_roles documentation Using "member" here can mislead a reader into believing this restriction is referring to the member being edited rather than the client/bot that is executing the edit. --- discord/member.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/member.py b/discord/member.py index 66c7715721d9..388e854835e5 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1076,7 +1076,7 @@ async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomi You must have :attr:`~Permissions.manage_roles` to use this, and the added :class:`Role`\s must appear lower in the list - of roles than the highest role of the member. + of roles than the highest role of the client. Parameters ----------- @@ -1115,7 +1115,7 @@ async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, at You must have :attr:`~Permissions.manage_roles` to use this, and the removed :class:`Role`\s must appear lower in the list - of roles than the highest role of the member. + of roles than the highest role of the client. Parameters ----------- From 5c4c281f05c23588e579740542a6bda0ccbd44f9 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 23 Nov 2024 21:48:45 -0500 Subject: [PATCH 8/9] Sanitize invite argument before calling the invite info endpoint Fixes a potential path traversal bug that can lead you to superfluously and erroneously call a separate endpoint. --- discord/utils.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/discord/utils.py b/discord/utils.py index 5d898b38bd34..905735cfb406 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -868,6 +868,12 @@ def resolve_invite(invite: Union[Invite, str]) -> ResolvedInvite: invite: Union[:class:`~discord.Invite`, :class:`str`] The invite. + Raises + ------- + ValueError + The invite is not a valid Discord invite, e.g. is not a URL + or does not contain alphanumeric characters. + Returns -------- :class:`.ResolvedInvite` @@ -887,7 +893,12 @@ def resolve_invite(invite: Union[Invite, str]) -> ResolvedInvite: event_id = url.query.get('event') return ResolvedInvite(code, int(event_id) if event_id else None) - return ResolvedInvite(invite, None) + + allowed_characters = r'[a-zA-Z0-9\-_]+' + if not re.fullmatch(allowed_characters, invite): + raise ValueError('Invite contains characters that are not allowed') + + return ResolvedInvite(invite, None) def resolve_template(code: Union[Template, str]) -> str: From e1b6310ef387481a654a1085653a33aa1b17e034 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 23 Nov 2024 22:04:57 -0500 Subject: [PATCH 9/9] Remove / from being safe from URI encoding when constructing paths --- discord/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index c66132055413..ac1254e3f052 100644 --- a/discord/http.py +++ b/discord/http.py @@ -309,7 +309,7 @@ def __init__(self, method: str, path: str, *, metadata: Optional[str] = None, ** self.metadata: Optional[str] = metadata url = self.BASE + self.path if parameters: - url = url.format_map({k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()}) + url = url.format_map({k: _uriquote(v, safe='') if isinstance(v, str) else v for k, v in parameters.items()}) self.url: str = url # major parameters: