From 3258507b4a9b2fdb9ff956f6fee8f941e030b851 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 24 Jan 2024 13:25:48 -0500 Subject: [PATCH 001/354] Call inner item callback and interaction_check by default --- discord/ui/dynamic.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/ui/dynamic.py b/discord/ui/dynamic.py index f3dcbf58a5ed..0b65e90f3a35 100644 --- a/discord/ui/dynamic.py +++ b/discord/ui/dynamic.py @@ -208,3 +208,9 @@ async def from_custom_id( from the ``match`` object. """ raise NotImplementedError + + async def callback(self, interaction: Interaction[ClientT]) -> Any: + return await self.item.callback(interaction) + + async def interaction_check(self, interaction: Interaction[ClientT], /) -> bool: + return await self.item.interaction_check(interaction) From 9859a3959b776bbe38f79bfcf5b06feb53cd8f69 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 26 Jan 2024 09:21:29 -0500 Subject: [PATCH 002/354] Pin documentation dependencies --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1b0fbdcfea1d..fdbe37c81061 100644 --- a/setup.py +++ b/setup.py @@ -37,9 +37,9 @@ 'docs': [ 'sphinx==4.4.0', 'sphinxcontrib_trio==1.1.2', - 'sphinxcontrib-websupport', + 'sphinxcontrib-websupport==1.2.6', 'typing-extensions>=4.3,<5', - 'sphinx-inline-tabs', + 'sphinx-inline-tabs==2023.4.21', ], 'speed': [ 'orjson>=3.5.4', From 0fadddf7d074a9cdfeda0f9279a4ba08c932ccb5 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 26 Jan 2024 09:23:33 -0500 Subject: [PATCH 003/354] Downgrade sphinxcontrib-websupport version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fdbe37c81061..cdc503dd80f8 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ 'docs': [ 'sphinx==4.4.0', 'sphinxcontrib_trio==1.1.2', - 'sphinxcontrib-websupport==1.2.6', + 'sphinxcontrib-websupport==1.2.4', 'typing-extensions>=4.3,<5', 'sphinx-inline-tabs==2023.4.21', ], From 62a70c21b6c33fd9eec5ec44706e106a80a6d740 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 26 Jan 2024 10:21:34 -0500 Subject: [PATCH 004/354] Pin remaining documentation dependencies For some reason the Sphinx developers made breaking changes in minor versions --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index cdc503dd80f8..ff572400c400 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,14 @@ 'docs': [ 'sphinx==4.4.0', 'sphinxcontrib_trio==1.1.2', + # TODO: bump these when migrating to a newer Sphinx version 'sphinxcontrib-websupport==1.2.4', + 'sphinxcontrib-applehelp==1.0.4', + 'sphinxcontrib-devhelp==1.0.2', + 'sphinxcontrib-htmlhelp==2.0.1', + 'sphinxcontrib-jsmath==1.0.1', + 'sphinxcontrib-qthelp==1.0.3', + 'sphinxcontrib-serializinghtml==1.1.5', 'typing-extensions>=4.3,<5', 'sphinx-inline-tabs==2023.4.21', ], From 851c857a366435fcb2be696e0c53526e73dc3e88 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 27 Jan 2024 02:02:11 +0100 Subject: [PATCH 005/354] Add support for guild incidents Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/guild.py | 85 ++++++++++++++++++++++++++++++++++++++++++ discord/http.py | 3 ++ discord/types/guild.py | 6 +++ 3 files changed, 94 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 82692ff73388..416530f3a43a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -109,6 +109,7 @@ Guild as GuildPayload, RolePositionUpdate as RolePositionUpdatePayload, GuildFeature, + IncidentData, ) from .types.threads import ( Thread as ThreadPayload, @@ -320,6 +321,7 @@ class Guild(Hashable): 'premium_progress_bar_enabled', '_safety_alerts_channel_id', 'max_stage_video_users', + '_incidents_data', ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { @@ -509,6 +511,7 @@ def _from_data(self, guild: GuildPayload) -> None: self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250 self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') + self._incidents_data: Optional[IncidentData] = guild.get('incidents_data') if 'channels' in guild: channels = guild['channels'] @@ -1843,6 +1846,8 @@ async def edit( mfa_level: MFALevel = MISSING, raid_alerts_disabled: bool = MISSING, safety_alerts_channel: TextChannel = MISSING, + invites_disabled_until: datetime.datetime = MISSING, + dms_disabled_until: datetime.datetime = MISSING, ) -> Guild: r"""|coro| @@ -1969,6 +1974,18 @@ async def edit( .. versionadded:: 2.3 + invites_disabled_until: Optional[:class:`datetime.datetime`] + The time when invites should be enabled again, or ``None`` to disable the action. + This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. + + .. versionadded:: 2.4 + + dms_disabled_until: Optional[:class:`datetime.datetime`] + The time when direct messages should be allowed again, or ``None`` to disable the action. + This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. + + .. versionadded:: 2.4 + Raises ------- Forbidden @@ -2157,6 +2174,30 @@ async def edit( await http.edit_guild_mfa_level(self.id, mfa_level=mfa_level.value) + incident_actions_payload: IncidentData = {} + if invites_disabled_until is not MISSING: + if invites_disabled_until is None: + incident_actions_payload['invites_disabled_until'] = None + else: + if invites_disabled_until.tzinfo is None: + raise TypeError( + 'invites_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + incident_actions_payload['invites_disabled_until'] = invites_disabled_until.isoformat() + + if dms_disabled_until is not MISSING: + if dms_disabled_until is None: + incident_actions_payload['dms_disabled_until'] = None + else: + if dms_disabled_until.tzinfo is None: + raise TypeError( + 'dms_disabled_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' + ) + incident_actions_payload['dms_disabled_until'] = dms_disabled_until.isoformat() + + if incident_actions_payload: + await http.edit_incident_actions(self.id, payload=incident_actions_payload) + data = await http.edit_guild(self.id, reason=reason, **fields) return Guild(data=data, state=self._state) @@ -4292,3 +4333,47 @@ async def create_automod_rule( ) return AutoModRule(data=data, guild=self, state=self._state) + + @property + def invites_paused_until(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: If invites are paused, returns when + invites will get enabled in UTC, otherwise returns None. + + .. versionadded:: 2.4 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('invites_disabled_until')) + + @property + def dms_paused_until(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: If DMs are paused, returns when DMs + will get enabled in UTC, otherwise returns None. + + .. versionadded:: 2.4 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('dms_disabled_until')) + + def invites_paused(self) -> bool: + """:class:`bool`: Whether invites are paused in the guild. + + .. versionadded:: 2.4 + """ + if not self.invites_paused_until: + return False + + return self.invites_paused_until > utils.utcnow() + + def dms_paused(self) -> bool: + """:class:`bool`: Whether DMs are paused in the guild. + + .. versionadded:: 2.4 + """ + if not self.dms_paused_until: + return False + + return self.dms_paused_until > utils.utcnow() diff --git a/discord/http.py b/discord/http.py index 69c5c779952a..e97bb883e773 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1764,6 +1764,9 @@ def edit_widget( ) -> Response[widget.WidgetSettings]: return self.request(Route('PATCH', '/guilds/{guild_id}/widget', guild_id=guild_id), json=payload, reason=reason) + def edit_incident_actions(self, guild_id: Snowflake, payload: guild.IncidentData) -> Response[guild.IncidentData]: + return self.request(Route('PUT', '/guilds/{guild_id}/incident-actions', guild_id=guild_id), json=payload) + # Invite management def create_invite( diff --git a/discord/types/guild.py b/discord/types/guild.py index 44d51019a4fc..95fa2d56ea74 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -49,6 +49,11 @@ class UnavailableGuild(TypedDict): unavailable: NotRequired[bool] +class IncidentData(TypedDict): + invites_disabled_until: NotRequired[Optional[str]] + dms_disabled_until: NotRequired[Optional[str]] + + DefaultMessageNotificationLevel = Literal[0, 1] ExplicitContentFilterLevel = Literal[0, 1, 2] MFALevel = Literal[0, 1] @@ -97,6 +102,7 @@ class _BaseGuildPreview(UnavailableGuild): stickers: List[GuildSticker] features: List[GuildFeature] description: Optional[str] + incidents_data: Optional[IncidentData] class _GuildPreviewUnique(TypedDict): From 841e9157bde08406720e4d6b2f135f62697c2b43 Mon Sep 17 00:00:00 2001 From: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:02:53 -0500 Subject: [PATCH 006/354] Add Python 3.12 to list of supported versions --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ff572400c400..ffe3057fe305 100644 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Internet', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', From 070ae24d8d133768f26b6219562ad3544a536cad Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sat, 27 Jan 2024 02:08:00 +0100 Subject: [PATCH 007/354] Document all limits in discord.ui --- discord/components.py | 8 +++----- discord/ui/button.py | 4 ++++ discord/ui/modal.py | 3 ++- discord/ui/select.py | 22 +++++++++++++++++++++- discord/ui/text_input.py | 6 ++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/discord/components.py b/discord/components.py index dca22e8bec65..43a8f6ffc6bf 100644 --- a/discord/components.py +++ b/discord/components.py @@ -318,8 +318,8 @@ class SelectOption: Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. - If not provided when constructed then it defaults to the - label. Can only be up to 100 characters. + If not provided when constructed then it defaults to the label. + Can only be up to 100 characters. description: Optional[:class:`str`] An additional description of the option, if any. Can only be up to 100 characters. @@ -332,14 +332,12 @@ class SelectOption: ----------- label: :class:`str` The label of the option. This is displayed to users. - Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. If not provided when constructed then it defaults to the - label. Can only be up to 100 characters. + label. description: Optional[:class:`str`] An additional description of the option, if any. - Can only be up to 100 characters. default: :class:`bool` Whether this option is selected by default. """ diff --git a/discord/ui/button.py b/discord/ui/button.py index 2c051d12cb03..28238a6f06aa 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -61,12 +61,14 @@ class Button(Item[V]): custom_id: Optional[:class:`str`] The ID of the button that gets received during an interaction. If this button is for a URL, it does not have a custom ID. + Can only be up to 100 characters. url: Optional[:class:`str`] The URL this button sends you to. disabled: :class:`bool` Whether the button is disabled or not. label: Optional[:class:`str`] The label of the button, if any. + Can only be up to 80 characters. emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] The emoji of the button, if available. row: Optional[:class:`int`] @@ -258,9 +260,11 @@ def button( ------------ label: Optional[:class:`str`] The label of the button, if any. + Can only be up to 80 characters. custom_id: Optional[:class:`str`] The ID of the button that gets received during an interaction. It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. style: :class:`.ButtonStyle` The style of the button. Defaults to :attr:`.ButtonStyle.grey`. disabled: :class:`bool` diff --git a/discord/ui/modal.py b/discord/ui/modal.py index b26fa9335b6c..630fc20f0c99 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -77,7 +77,8 @@ async def on_submit(self, interaction: discord.Interaction): Parameters ----------- title: :class:`str` - The title of the modal. Can only be up to 45 characters. + The title of the modal. + Can only be up to 45 characters. timeout: Optional[:class:`float`] Timeout in seconds from last interaction with the UI before no longer accepting input. If ``None`` then there is no timeout. diff --git a/discord/ui/select.py b/discord/ui/select.py index 47c9d4a4779c..294a539dd489 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -366,8 +366,10 @@ class Select(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -376,6 +378,7 @@ class Select(BaseSelect[V]): Defaults to 1 and must be between 1 and 25. options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. + Can only contain up to 25 items. disabled: :class:`bool` Whether the select is disabled or not. row: Optional[:class:`int`] @@ -455,7 +458,8 @@ def add_option( Can only be up to 100 characters. value: :class:`str` The value of the option. This is not displayed to users. - If not given, defaults to the label. Can only be up to 100 characters. + If not given, defaults to the label. + Can only be up to 100 characters. description: Optional[:class:`str`] An additional description of the option, if any. Can only be up to 100 characters. @@ -515,8 +519,10 @@ class UserSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -527,6 +533,7 @@ class UserSelect(BaseSelect[V]): Whether the select is disabled or not. default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the users that should be selected by default. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -604,8 +611,10 @@ class RoleSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -616,6 +625,7 @@ class RoleSelect(BaseSelect[V]): Whether the select is disabled or not. default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the roles that should be selected by default. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -688,8 +698,10 @@ class MentionableSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -701,6 +713,7 @@ class MentionableSelect(BaseSelect[V]): default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the users/roles that should be selected by default. if :class:`.Object` is passed, then the type must be specified in the constructor. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -778,10 +791,12 @@ class ChannelSelect(BaseSelect[V]): custom_id: :class:`str` The ID of the select menu that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. channel_types: List[:class:`~discord.ChannelType`] The types of channels to show in the select menu. Defaults to all channels. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. min_values: :class:`int` The minimum number of items that must be chosen for this select menu. Defaults to 1 and must be between 0 and 25. @@ -792,6 +807,7 @@ class ChannelSelect(BaseSelect[V]): Whether the select is disabled or not. default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the channels that should be selected by default. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 row: Optional[:class:`int`] @@ -1011,9 +1027,11 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe get overridden. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. custom_id: :class:`str` The ID of the select menu that gets received during an interaction. It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. row: Optional[:class:`int`] The relative row this select menu belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd @@ -1029,6 +1047,7 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe options: List[:class:`discord.SelectOption`] A list of options that can be selected in this menu. This can only be used with :class:`Select` instances. + Can only contain up to 25 items. channel_types: List[:class:`~discord.ChannelType`] The types of channels to show in the select menu. Defaults to all channels. This can only be used with :class:`ChannelSelect` instances. @@ -1037,6 +1056,7 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe default_values: Sequence[:class:`~discord.abc.Snowflake`] A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. + Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 """ diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 23c1d874f285..96b4581f40b0 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -65,21 +65,27 @@ class TextInput(Item[V]): ------------ label: :class:`str` The label to display above the text input. + Can only be up to 45 characters. custom_id: :class:`str` The ID of the text input that gets received during an interaction. If not given then one is generated for you. + Can only be up to 100 characters. style: :class:`discord.TextStyle` The style of the text input. placeholder: Optional[:class:`str`] The placeholder text to display when the text input is empty. + Can only be up to 100 characters. default: Optional[:class:`str`] The default value of the text input. + Can only be up to 4000 characters. required: :class:`bool` Whether the text input is required. min_length: Optional[:class:`int`] The minimum length of the text input. + Must be between 0 and 4000. max_length: Optional[:class:`int`] The maximum length of the text input. + Must be between 1 and 4000. row: Optional[:class:`int`] The relative row this text input belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd From e25b7ff3f83f09ee6803b7884b2c7ee80a0c11d2 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 27 Jan 2024 02:12:07 +0100 Subject: [PATCH 008/354] Support for avatar decorations Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> --- discord/abc.py | 16 ++++++++++++++++ discord/asset.py | 9 +++++++++ discord/member.py | 29 ++++++++++++++++++++++++++--- discord/types/gateway.py | 3 ++- discord/types/member.py | 5 ++++- discord/types/user.py | 7 +++++++ discord/user.py | 35 ++++++++++++++++++++++++++++++----- discord/webhook/async_.py | 3 ++- 8 files changed, 96 insertions(+), 11 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 64a0b0041210..8eeb9d4d0f58 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -248,6 +248,22 @@ def avatar(self) -> Optional[Asset]: """Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar, if present.""" raise NotImplementedError + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar decoration, if present. + + .. versionadded:: 2.4 + """ + raise NotImplementedError + + @property + def avatar_decoration_sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns an integer that represents the user's avatar decoration SKU ID, if present. + + .. versionadded:: 2.4 + """ + raise NotImplementedError + @property def default_avatar(self) -> Asset: """:class:`~discord.Asset`: Returns the default avatar for a given user.""" diff --git a/discord/asset.py b/discord/asset.py index d88ebb945d44..d08635632015 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -246,6 +246,15 @@ def _from_guild_avatar(cls, state: _State, guild_id: int, member_id: int, avatar animated=animated, ) + @classmethod + def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self: + return cls( + state, + url=f'{cls.BASE}/avatar-decoration-presets/{avatar_decoration}.png?size=96', + key=avatar_decoration, + animated=True, + ) + @classmethod def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self: return cls( diff --git a/discord/member.py b/discord/member.py index 71231e426ca1..4cfa54a359bd 100644 --- a/discord/member.py +++ b/discord/member.py @@ -67,7 +67,7 @@ UserWithMember as UserWithMemberPayload, ) from .types.gateway import GuildMemberUpdateEvent - from .types.user import User as UserPayload + from .types.user import User as UserPayload, AvatarDecorationData from .abc import Snowflake from .state import ConnectionState from .message import Message @@ -323,6 +323,7 @@ class Member(discord.abc.Messageable, _UserTag): '_state', '_avatar', '_flags', + '_avatar_decoration_data', ) if TYPE_CHECKING: @@ -342,6 +343,8 @@ class Member(discord.abc.Messageable, _UserTag): banner: Optional[Asset] accent_color: Optional[Colour] accent_colour: Optional[Colour] + avatar_decoration: Optional[Asset] + avatar_decoration_sku_id: Optional[int] def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState): self._state: ConnectionState = state @@ -357,6 +360,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self._avatar: Optional[str] = data.get('avatar') self._permissions: Optional[int] self._flags: int = data['flags'] + self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') try: self._permissions = int(data['permissions']) except KeyError: @@ -425,6 +429,7 @@ def _copy(cls, member: Self) -> Self: self._permissions = member._permissions self._state = member._state self._avatar = member._avatar + self._avatar_decoration_data = member._avatar_decoration_data # Reference will not be copied unless necessary by PRESENCE_UPDATE # See below @@ -453,6 +458,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self._roles = utils.SnowflakeList(map(int, data['roles'])) self._avatar = data.get('avatar') self._flags = data.get('flags', 0) + self._avatar_decoration_data = data.get('avatar_decoration_data') def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: self.activities = tuple(create_activity(d, self._state) for d in data['activities']) @@ -464,7 +470,16 @@ def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Op def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user - original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags) + original = ( + u.name, + u.discriminator, + u._avatar, + u.global_name, + u._public_flags, + u._avatar_decoration_data['sku_id'] if u._avatar_decoration_data is not None else None, + ) + + decoration_payload = user.get('avatar_decoration_data') # These keys seem to always be available modified = ( user['username'], @@ -472,10 +487,18 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: user['avatar'], user.get('global_name'), user.get('public_flags', 0), + decoration_payload['sku_id'] if decoration_payload is not None else None, ) if original != modified: to_return = User._copy(self._user) - u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified + u.name, u.discriminator, u._avatar, u.global_name, u._public_flags, u._avatar_decoration_data = ( + user['username'], + user['discriminator'], + user['avatar'], + user.get('global_name'), + user.get('public_flags', 0), + decoration_payload, + ) # Signal to dispatch on_user_update return to_return, u diff --git a/discord/types/gateway.py b/discord/types/gateway.py index fb450017e9c2..c0908435f9f2 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -41,7 +41,7 @@ from .sticker import GuildSticker from .appinfo import GatewayAppInfo, PartialAppInfo from .guild import Guild, UnavailableGuild -from .user import User +from .user import User, AvatarDecorationData from .threads import Thread, ThreadMember from .scheduled_event import GuildScheduledEvent from .audit_log import AuditLogEntry @@ -228,6 +228,7 @@ class GuildMemberUpdateEvent(TypedDict): mute: NotRequired[bool] pending: NotRequired[bool] communication_disabled_until: NotRequired[str] + avatar_decoration_data: NotRequired[AvatarDecorationData] class GuildEmojisUpdateEvent(TypedDict): diff --git a/discord/types/member.py b/discord/types/member.py index ad9e49008a12..6968edb6f47f 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -24,7 +24,8 @@ from typing import Optional, TypedDict from .snowflake import SnowflakeList -from .user import User +from .user import User, AvatarDecorationData +from typing_extensions import NotRequired class Nickname(TypedDict): @@ -47,6 +48,7 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + avatar_decoration_data: NotRequired[AvatarDecorationData] class _OptionalMemberWithUser(PartialMember, total=False): @@ -56,6 +58,7 @@ class _OptionalMemberWithUser(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + avatar_decoration_data: NotRequired[AvatarDecorationData] class MemberWithUser(_OptionalMemberWithUser): diff --git a/discord/types/user.py b/discord/types/user.py index 7a34e44bb786..1f027ce9d9ac 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -24,6 +24,12 @@ from .snowflake import Snowflake from typing import Literal, Optional, TypedDict +from typing_extensions import NotRequired + + +class AvatarDecorationData(TypedDict): + asset: str + sku_id: Snowflake class PartialUser(TypedDict): @@ -32,6 +38,7 @@ class PartialUser(TypedDict): discriminator: str avatar: Optional[str] global_name: Optional[str] + avatar_decoration_data: NotRequired[AvatarDecorationData] PremiumType = Literal[0, 1, 2, 3] diff --git a/discord/user.py b/discord/user.py index cc836374a40b..b0ba869cbd37 100644 --- a/discord/user.py +++ b/discord/user.py @@ -31,7 +31,7 @@ from .colour import Colour from .enums import DefaultAvatar from .flags import PublicUserFlags -from .utils import snowflake_time, _bytes_to_base64_data, MISSING +from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake if TYPE_CHECKING: from typing_extensions import Self @@ -43,10 +43,7 @@ from .message import Message from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload - from .types.user import ( - PartialUser as PartialUserPayload, - User as UserPayload, - ) + from .types.user import PartialUser as PartialUserPayload, User as UserPayload, AvatarDecorationData __all__ = ( @@ -73,6 +70,7 @@ class BaseUser(_UserTag): 'system', '_public_flags', '_state', + '_avatar_decoration_data', ) if TYPE_CHECKING: @@ -87,6 +85,7 @@ class BaseUser(_UserTag): _banner: Optional[str] _accent_colour: Optional[int] _public_flags: int + _avatar_decoration_data: Optional[AvatarDecorationData] def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: self._state = state @@ -123,6 +122,7 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None: self._public_flags = data.get('public_flags', 0) self.bot = data.get('bot', False) self.system = data.get('system', False) + self._avatar_decoration_data = data.get('avatar_decoration_data') @classmethod def _copy(cls, user: Self) -> Self: @@ -138,6 +138,7 @@ def _copy(cls, user: Self) -> Self: self.bot = user.bot self._state = user._state self._public_flags = user._public_flags + self._avatar_decoration_data = user._avatar_decoration_data return self @@ -187,6 +188,30 @@ def display_avatar(self) -> Asset: """ return self.avatar or self.default_avatar + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar decoration the user has. + + If the user has not set an avatar decoration, ``None`` is returned. + + .. versionadded:: 2.4 + """ + if self._avatar_decoration_data is not None: + return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data['asset']) + return None + + @property + def avatar_decoration_sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns the SKU ID of the avatar decoration the user has. + + If the user has not set an avatar decoration, ``None`` is returned. + + .. versionadded:: 2.4 + """ + if self._avatar_decoration_data is not None: + return _get_as_snowflake(self._avatar_decoration_data, 'sku_id') + return None + @property def banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the user's banner asset, if available. diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2a9a649e5514..e2f37e5e62e5 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1305,9 +1305,10 @@ def _as_follower(cls, data, *, channel, user) -> Self: 'user': { 'username': user.name, 'discriminator': user.discriminator, - 'global_name': user.global_name, 'id': user.id, 'avatar': user._avatar, + 'avatar_decoration_data': user._avatar_decoration_data, + 'global_name': user.global_name, }, } From a9ff58724b5c0cf51b00f804ea185beaa5840da8 Mon Sep 17 00:00:00 2001 From: Imayhaveborkedit Date: Fri, 26 Jan 2024 20:14:19 -0500 Subject: [PATCH 009/354] Fix move_to related voice state bugs --- discord/gateway.py | 2 +- discord/voice_state.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index dc52828134a9..b8936bf5708b 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -827,7 +827,7 @@ def __init__( self.loop: asyncio.AbstractEventLoop = loop self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._close_code: Optional[int] = None - self.secret_key: Optional[str] = None + self.secret_key: Optional[List[int]] = None if hook: self._hook = hook diff --git a/discord/voice_state.py b/discord/voice_state.py index 6a680a106303..8ea83c6519e6 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -267,26 +267,31 @@ async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: return + channel_id = int(channel_id) self.session_id = data['session_id'] # we got the event while connecting if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update): if self.state is ConnectionFlowState.set_guild_voice_state: self.state = ConnectionFlowState.got_voice_state_update + + # we moved ourselves + if channel_id != self.voice_client.channel.id: + self._update_voice_channel(channel_id) + else: self.state = ConnectionFlowState.got_both_voice_updates return if self.state is ConnectionFlowState.connected: - self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore + self._update_voice_channel(channel_id) elif self.state is not ConnectionFlowState.disconnected: if channel_id != self.voice_client.channel.id: # For some unfortunate reason we were moved during the connection flow _log.info('Handling channel move while connecting...') - self.voice_client.channel = channel_id and self.guild.get_channel(int(channel_id)) # type: ignore - + self._update_voice_channel(channel_id) await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update) await self.connect( reconnect=self.reconnect, @@ -484,6 +489,9 @@ async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[floa await self.disconnect() return + if self.voice_client.channel and channel.id == self.voice_client.channel.id: + return + previous_state = self.state # this is only an outgoing ws request # if it fails, nothing happens and nothing changes (besides self.state) @@ -637,3 +645,6 @@ async def _potential_reconnect(self) -> bool: async def _move_to(self, channel: abc.Snowflake) -> None: await self.voice_client.channel.guild.change_voice_state(channel=channel) self.state = ConnectionFlowState.set_guild_voice_state + + def _update_voice_channel(self, channel_id: Optional[int]) -> None: + self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore From bd402b486cc12f0c1bf7377fd65f2fe0a8fabd73 Mon Sep 17 00:00:00 2001 From: Akai <93782497+AmazingAkai@users.noreply.github.com> Date: Sat, 27 Jan 2024 07:26:02 +0530 Subject: [PATCH 010/354] Correct typo in error message: "error" to "autocomplete" --- discord/app_commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 6c2aae7b8df6..6f46fbe4c978 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -1068,7 +1068,7 @@ async def fruits_autocomplete( def decorator(coro: AutocompleteCallback[GroupT, ChoiceT]) -> AutocompleteCallback[GroupT, ChoiceT]: if not inspect.iscoroutinefunction(coro): - raise TypeError('The error handler must be a coroutine.') + raise TypeError('The autocomplete callback must be a coroutine function.') try: param = self._params[name] From 8d3b56206b48f0215bcb5be0e94c18afe7277975 Mon Sep 17 00:00:00 2001 From: JDJG Date: Wed, 7 Feb 2024 19:38:18 -0500 Subject: [PATCH 011/354] Update ClientUser.edit docs for animated avatars --- discord/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/user.py b/discord/user.py index b0ba869cbd37..2cffcee559f7 100644 --- a/discord/user.py +++ b/discord/user.py @@ -410,7 +410,6 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI then the file must be opened via ``open('some_filename', 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``. - The only image formats supported for uploading is JPEG and PNG. .. versionchanged:: 2.0 The edit is no longer in-place, instead the newly edited client user is returned. @@ -426,6 +425,7 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI avatar: Optional[:class:`bytes`] A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. Raises ------ From 9345a2a1be550cfb36330f738c23b9866bc8a0df Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Feb 2024 05:54:48 -0500 Subject: [PATCH 012/354] Add warning for FFmpeg spawning classes executable parameter --- discord/player.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord/player.py b/discord/player.py index 147c0628a533..b2158d8faeff 100644 --- a/discord/player.py +++ b/discord/player.py @@ -288,6 +288,12 @@ class FFmpegPCMAudio(FFmpegAudio): passed to the stdin of ffmpeg. executable: :class:`str` The executable name (and path) to use. Defaults to ``ffmpeg``. + + .. warning:: + + Since this class spawns a subprocess, care should be taken to not + pass in an arbitrary executable name when using this parameter. + pipe: :class:`bool` If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``. @@ -392,6 +398,12 @@ class FFmpegOpusAudio(FFmpegAudio): executable: :class:`str` The executable name (and path) to use. Defaults to ``ffmpeg``. + + .. warning:: + + Since this class spawns a subprocess, care should be taken to not + pass in an arbitrary executable name when using this parameter. + pipe: :class:`bool` If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``. From 76666fbcf49cff6ea6dd21dda75d75d7dc5d3c19 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 17 Feb 2024 01:17:31 -0500 Subject: [PATCH 013/354] Properly check for maximum number of children --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index cff11d0905cc..f3eda6a60485 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -320,7 +320,7 @@ def add_item(self, item: Item[Any]) -> Self: or the row the item is trying to be added to is full. """ - if len(self._children) > 25: + if len(self._children) >= 25: raise ValueError('maximum number of children exceeded') if not isinstance(item, Item): From 4e03b170ef0c54167d679ac6cd077a7ba984e026 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Wed, 21 Feb 2024 05:36:02 +1100 Subject: [PATCH 014/354] Update pyright version --- .github/workflows/lint.yml | 2 +- discord/app_commands/checks.py | 2 +- discord/app_commands/transformers.py | 8 ++++---- discord/asset.py | 6 +++--- discord/automod.py | 2 +- discord/client.py | 2 +- discord/colour.py | 2 +- discord/enums.py | 12 ++++++++---- discord/ext/commands/cog.py | 4 ++-- discord/ext/commands/converter.py | 2 +- discord/ext/commands/core.py | 6 +++--- discord/ext/commands/flags.py | 2 +- discord/ext/commands/hybrid.py | 2 +- discord/file.py | 2 +- discord/ui/select.py | 16 ++++++++-------- discord/utils.py | 2 +- discord/welcome_screen.py | 2 +- 17 files changed, 39 insertions(+), 35 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b233a5deff09..9e70f794faab 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,7 +38,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: - version: '1.1.316' + version: '1.1.351' warnings: false no-comments: ${{ matrix.python-version != '3.x' }} diff --git a/discord/app_commands/checks.py b/discord/app_commands/checks.py index f6c09481d549..5c17b951c294 100644 --- a/discord/app_commands/checks.py +++ b/discord/app_commands/checks.py @@ -186,7 +186,7 @@ def copy(self) -> Self: :class:`Cooldown` A new instance of this cooldown. """ - return Cooldown(self.rate, self.per) + return self.__class__(self.rate, self.per) def __repr__(self) -> str: return f'' diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index 59b3af758310..d012c52b98af 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -638,7 +638,7 @@ def __init__(self, *channel_types: Type[Any]) -> None: except KeyError: raise TypeError('Union type of channels must be entirely made up of channels') from None - self._types: Tuple[Type[Any]] = channel_types + self._types: Tuple[Type[Any], ...] = channel_types self._channel_types: List[ChannelType] = types self._display_name = display_name @@ -780,11 +780,11 @@ def get_supported_annotation( # Check if there's an origin origin = getattr(annotation, '__origin__', None) if origin is Literal: - args = annotation.__args__ # type: ignore + args = annotation.__args__ return (LiteralTransformer(args), MISSING, True) if origin is Choice: - arg = annotation.__args__[0] # type: ignore + arg = annotation.__args__[0] return (ChoiceTransformer(arg), MISSING, True) if origin is not Union: @@ -792,7 +792,7 @@ def get_supported_annotation( raise TypeError(f'unsupported type annotation {annotation!r}') default = MISSING - args = annotation.__args__ # type: ignore + args = annotation.__args__ if args[-1] is _none: if len(args) == 2: underlying = args[0] diff --git a/discord/asset.py b/discord/asset.py index d08635632015..7b9b711334a3 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -429,7 +429,7 @@ def replace( url = url.with_query(url.raw_query_string) url = str(url) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_size(self, size: int, /) -> Self: """Returns a new asset with the specified size. @@ -457,7 +457,7 @@ def with_size(self, size: int, /) -> Self: raise ValueError('size must be a power of 2 between 16 and 4096') url = str(yarl.URL(self._url).with_query(size=size)) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_format(self, format: ValidAssetFormatTypes, /) -> Self: """Returns a new asset with the specified format. @@ -492,7 +492,7 @@ def with_format(self, format: ValidAssetFormatTypes, /) -> Self: url = yarl.URL(self._url) path, _ = os.path.splitext(url.path) url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string)) - return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + return self.__class__(state=self._state, url=url, key=self._key, animated=self._animated) def with_static_format(self, format: ValidStaticFormatTypes, /) -> Self: """Returns a new asset with the specified static format. diff --git a/discord/automod.py b/discord/automod.py index b20f9016a12a..eabf42806be3 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -541,7 +541,7 @@ async def edit( **payload, ) - return AutoModRule(data=data, guild=self.guild, state=self._state) + return self.__class__(data=data, guild=self.guild, state=self._state) async def delete(self, *, reason: str = MISSING) -> None: """|coro| diff --git a/discord/client.py b/discord/client.py index ccafc073d1ea..b3243ef93bd4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -315,7 +315,7 @@ async def __aexit__( def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket: return self.ws - def _get_state(self, **options: Any) -> ConnectionState: + def _get_state(self, **options: Any) -> ConnectionState[Self]: return ConnectionState(dispatch=self.dispatch, handlers=self._handlers, hooks=self._hooks, http=self.http, **options) def _handle_ready(self) -> None: diff --git a/discord/colour.py b/discord/colour.py index 5772b145d426..7e3a37132a11 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -175,7 +175,7 @@ def from_hsv(cls, h: float, s: float, v: float) -> Self: return cls.from_rgb(*(int(x * 255) for x in rgb)) @classmethod - def from_str(cls, value: str) -> Self: + def from_str(cls, value: str) -> Colour: """Constructs a :class:`Colour` from a string. The following formats are accepted: diff --git a/discord/enums.py b/discord/enums.py index ed498b8be190..0c6f93fdf2ec 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -75,9 +75,6 @@ 'EntitlementOwnerType', ) -if TYPE_CHECKING: - from typing_extensions import Self - def _create_value_cls(name: str, comparable: bool): # All the type ignores here are due to the type checker being unable to recognise @@ -104,7 +101,14 @@ class EnumMeta(type): _enum_member_map_: ClassVar[Dict[str, Any]] _enum_value_map_: ClassVar[Dict[Any, Any]] - def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any], *, comparable: bool = False) -> Self: + def __new__( + cls, + name: str, + bases: Tuple[type, ...], + attrs: Dict[str, Any], + *, + comparable: bool = False, + ) -> EnumMeta: value_mapping = {} member_mapping = {} member_names = [] diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 1ffe25c702bb..54842c2599a9 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -169,7 +169,7 @@ async def bar(self, ctx): __cog_app_commands__: List[Union[app_commands.Group, app_commands.Command[Any, ..., Any]]] __cog_listeners__: List[Tuple[str, str]] - def __new__(cls, *args: Any, **kwargs: Any) -> Self: + def __new__(cls, *args: Any, **kwargs: Any) -> CogMeta: name, bases, attrs = args if any(issubclass(base, app_commands.Group) for base in bases): raise TypeError( @@ -366,7 +366,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: child.wrapped = lookup[child.qualified_name] # type: ignore if self.__cog_app_commands_group__: - children.append(app_command) # type: ignore # Somehow it thinks it can be None here + children.append(app_command) if Cog._get_overridden_method(self.cog_app_command_error) is not None: error_handler = self.cog_app_command_error diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 7255f171540b..41e0f6c4a5b5 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1185,7 +1185,7 @@ def _convert_to_bool(argument: str) -> bool: raise BadBoolArgument(lowered) -_GenericAlias = type(List[T]) +_GenericAlias = type(List[T]) # type: ignore def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index efd7b09d29fc..1c682a957725 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -461,7 +461,7 @@ def __init__( # bandaid for the fact that sometimes parent can be the bot instance parent: Optional[GroupMixin[Any]] = kwargs.get('parent') - self.parent: Optional[GroupMixin[Any]] = parent if isinstance(parent, _BaseCommand) else None # type: ignore # Does not recognise mixin usage + self.parent: Optional[GroupMixin[Any]] = parent if isinstance(parent, _BaseCommand) else None self._before_invoke: Optional[Hook] = None try: @@ -776,7 +776,7 @@ def full_parent_name(self) -> str: command = self # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO while command.parent is not None: # type: ignore - command = command.parent + command = command.parent # type: ignore entries.append(command.name) # type: ignore return ' '.join(reversed(entries)) @@ -794,7 +794,7 @@ def parents(self) -> List[Group[Any, ..., Any]]: entries = [] command = self while command.parent is not None: # type: ignore - command = command.parent + command = command.parent # type: ignore entries.append(command) return entries diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 280b652573ef..dee00fa7e5c6 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -280,7 +280,7 @@ def __new__( case_insensitive: bool = MISSING, delimiter: str = MISSING, prefix: str = MISSING, - ) -> Self: + ) -> FlagsMeta: attrs['__commands_is_flag__'] = True try: diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index a8775474dc9a..c9797e734eeb 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -902,7 +902,7 @@ def hybrid_command( def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridCommand[CogT, P, T]: if isinstance(func, Command): raise TypeError('Callback is already a command.') - return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore # ??? + return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) return decorator diff --git a/discord/file.py b/discord/file.py index 504d86b73a80..7e4df415b241 100644 --- a/discord/file.py +++ b/discord/file.py @@ -111,7 +111,7 @@ def __init__( else: filename = getattr(fp, 'name', 'untitled') - self._filename, filename_spoiler = _strip_spoiler(filename) + self._filename, filename_spoiler = _strip_spoiler(filename) # type: ignore # pyright doesn't understand the above getattr if spoiler is MISSING: spoiler = filename_spoiler diff --git a/discord/ui/select.py b/discord/ui/select.py index 294a539dd489..b7a8e694cf1c 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -69,7 +69,7 @@ ) if TYPE_CHECKING: - from typing_extensions import TypeAlias, Self, TypeGuard + from typing_extensions import TypeAlias, TypeGuard from .view import View from ..types.components import SelectMenu as SelectMenuPayload @@ -342,7 +342,7 @@ def is_dispatchable(self) -> bool: return True @classmethod - def from_component(cls, component: SelectMenu) -> Self: + def from_component(cls, component: SelectMenu) -> BaseSelect[V]: type_to_cls: Dict[ComponentType, Type[BaseSelect[Any]]] = { ComponentType.string_select: Select, ComponentType.user_select: UserSelect, @@ -887,7 +887,7 @@ def default_values(self, value: Sequence[ValidDefaultValues]) -> None: @overload def select( *, - cls: Type[SelectT] = Select[V], + cls: Type[SelectT] = Select[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -903,7 +903,7 @@ def select( @overload def select( *, - cls: Type[UserSelectT] = UserSelect[V], + cls: Type[UserSelectT] = UserSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -920,7 +920,7 @@ def select( @overload def select( *, - cls: Type[RoleSelectT] = RoleSelect[V], + cls: Type[RoleSelectT] = RoleSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -937,7 +937,7 @@ def select( @overload def select( *, - cls: Type[ChannelSelectT] = ChannelSelect[V], + cls: Type[ChannelSelectT] = ChannelSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = ..., placeholder: Optional[str] = ..., @@ -954,7 +954,7 @@ def select( @overload def select( *, - cls: Type[MentionableSelectT] = MentionableSelect[V], + cls: Type[MentionableSelectT] = MentionableSelect[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, placeholder: Optional[str] = ..., @@ -970,7 +970,7 @@ def select( def select( *, - cls: Type[BaseSelectT] = Select[V], + cls: Type[BaseSelectT] = Select[Any], options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, placeholder: Optional[str] = None, diff --git a/discord/utils.py b/discord/utils.py index 33a4020a2504..3509bf3ab19e 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -721,7 +721,7 @@ async def sane_wait_for(futures: Iterable[Awaitable[T]], *, timeout: Optional[fl def get_slots(cls: Type[Any]) -> Iterator[str]: for mro in reversed(cls.__mro__): try: - yield from mro.__slots__ # type: ignore + yield from mro.__slots__ except AttributeError: continue diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index d23392a548f5..1ca487c91a7d 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -214,4 +214,4 @@ async def edit( fields['enabled'] = enabled data = await self._state.http.edit_welcome_screen(self._guild.id, reason=reason, **fields) - return WelcomeScreen(data=data, guild=self._guild) + return self.__class__(data=data, guild=self._guild) From 61eddfcb189f11a293011d43b09fe4ec52641dd2 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Wed, 21 Feb 2024 03:29:24 +0530 Subject: [PATCH 015/354] Fix sticker URL for GIF stickers --- discord/sticker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/sticker.py b/discord/sticker.py index 225e7648a167..30eb62c70e23 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -277,7 +277,10 @@ def _from_data(self, data: StickerPayload) -> None: self.name: str = data['name'] self.description: str = data['description'] self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type']) - self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' + if self.format is StickerFormatType.gif: + self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif' + else: + self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' def __repr__(self) -> str: return f'' From 56916f924115a6001a5b2f8c6253df07c584d504 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 22 Feb 2024 20:42:14 -0500 Subject: [PATCH 016/354] Fix comparisons between two Object with types --- discord/object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/object.py b/discord/object.py index 2243a040836d..885ad4dc2735 100644 --- a/discord/object.py +++ b/discord/object.py @@ -102,7 +102,7 @@ def __repr__(self) -> str: return f'' def __eq__(self, other: object) -> bool: - if isinstance(other, self.type): + if isinstance(other, (self.type, self.__class__)): return self.id == other.id return NotImplemented From edf7ce2ab0204b0f599183112cef5dd7c57dd077 Mon Sep 17 00:00:00 2001 From: fretgfr <51489753+fretgfr@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:39:25 -0400 Subject: [PATCH 017/354] Update Guild.prune_members required permissions notes --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 416530f3a43a..7d639fe67f0d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2565,7 +2565,7 @@ async def prune_members( The inactive members are denoted if they have not logged on in ``days`` number of days and they have no roles. - You must have :attr:`~Permissions.kick_members` to do this. + You must have both :attr:`~Permissions.kick_members` and :attr:`~Permissions.manage_guild` to do this. To check how many members you would prune without actually pruning, see the :meth:`estimate_pruned_members` function. From 82d13e7b497ac8983a3567a715d5c528b2a895ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Thu, 14 Mar 2024 22:10:17 +0000 Subject: [PATCH 018/354] Add support for ClientUser editing banners --- discord/user.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/discord/user.py b/discord/user.py index 2cffcee559f7..5151957dc998 100644 --- a/discord/user.py +++ b/discord/user.py @@ -398,7 +398,9 @@ def _update(self, data: UserPayload) -> None: self._flags = data.get('flags', 0) self.mfa_enabled = data.get('mfa_enabled', False) - async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSING) -> ClientUser: + async def edit( + self, *, username: str = MISSING, avatar: Optional[bytes] = MISSING, banner: Optional[bytes] = MISSING + ) -> ClientUser: """|coro| Edits the current profile of the client. @@ -426,6 +428,12 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no banner. + Only image formats supported for uploading are JPEG, PNG, GIF and WEBP. + + .. versionadded:: 2.4 Raises ------ @@ -449,6 +457,12 @@ async def edit(self, *, username: str = MISSING, avatar: Optional[bytes] = MISSI else: payload['avatar'] = None + if banner is not MISSING: + if banner is not None: + payload['banner'] = _bytes_to_base64_data(banner) + else: + payload['banner'] = None + data: UserPayload = await self._state.http.edit_profile(payload) return ClientUser(state=self._state, data=data) From ded9c5d87b2398ba5049b7bcc31f168ead911cfd Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 15 Mar 2024 18:05:35 -0400 Subject: [PATCH 019/354] Add support for bulk banning members --- discord/guild.py | 58 ++++++++++++++++++++++++++++++++++++++++++ discord/http.py | 14 ++++++++++ discord/types/guild.py | 5 ++++ docs/api.rst | 19 ++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 7d639fe67f0d..e65899a54f63 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -34,6 +34,7 @@ Collection, Coroutine, Dict, + Iterable, List, Mapping, NamedTuple, @@ -146,6 +147,11 @@ class BanEntry(NamedTuple): user: User +class BulkBanResult(NamedTuple): + banned: List[Object] + failed: List[Object] + + class _GuildLimit(NamedTuple): emoji: int stickers: int @@ -3789,6 +3795,58 @@ async def unban(self, user: Snowflake, *, reason: Optional[str] = None) -> None: """ await self._state.http.unban(user.id, self.id, reason=reason) + async def bulk_ban( + self, + users: Iterable[Snowflake], + *, + reason: Optional[str] = None, + delete_message_seconds: int = 86400, + ) -> BulkBanResult: + """|coro| + + Bans multiple users from the guild. + + The users must meet the :class:`abc.Snowflake` abc. + + You must have :attr:`~Permissions.ban_members` to do this. + + .. versionadded:: 2.4 + + Parameters + ----------- + users: :class:`abc.Snowflake` + The user to ban from their guild. + delete_message_seconds: :class:`int` + The number of seconds worth of messages to delete from the user + in the guild. The minimum is 0 and the maximum is 604800 (7 days). + Defaults to 1 day. + reason: Optional[:class:`str`] + The reason the users got banned. + + Raises + ------- + Forbidden + You do not have the proper permissions to ban. + HTTPException + Banning failed. + + Returns + -------- + :class:`BulkBanResult` + The result of the bulk ban operation. + """ + + response = await self._state.http.bulk_ban( + self.id, + user_ids=[u.id for u in users], + delete_message_seconds=delete_message_seconds, + reason=reason, + ) + return BulkBanResult( + banned=[Object(id=int(user_id), type=User) for user_id in response.get('banned_users', []) or []], + failed=[Object(id=int(user_id), type=User) for user_id in response.get('failed_users', []) or []], + ) + @property def vanity_url(self) -> Optional[str]: """Optional[:class:`str`]: The Discord vanity invite URL for this guild, if available. diff --git a/discord/http.py b/discord/http.py index e97bb883e773..0979942a980e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1055,6 +1055,20 @@ def unban(self, user_id: Snowflake, guild_id: Snowflake, *, reason: Optional[str r = Route('DELETE', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, reason=reason) + def bulk_ban( + self, + guild_id: Snowflake, + user_ids: List[Snowflake], + delete_message_seconds: int = 86400, + reason: Optional[str] = None, + ) -> Response[guild.BulkBanUserResponse]: + r = Route('POST', '/guilds/{guild_id}/bulk-ban', guild_id=guild_id) + payload = { + 'user_ids': user_ids, + 'delete_message_seconds': delete_message_seconds, + } + return self.request(r, json=payload, reason=reason) + def guild_voice_state( self, user_id: Snowflake, diff --git a/discord/types/guild.py b/discord/types/guild.py index 95fa2d56ea74..ba43fbf96c14 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -185,3 +185,8 @@ class _RolePositionRequired(TypedDict): class RolePositionUpdate(_RolePositionRequired, total=False): position: Optional[Snowflake] + + +class BulkBanUserResponse(TypedDict): + banned_users: Optional[List[Snowflake]] + failed_users: Optional[List[Snowflake]] diff --git a/docs/api.rst b/docs/api.rst index 095739721e0d..f436b665b590 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4491,6 +4491,25 @@ Guild :type: :class:`User` +.. class:: BulkBanResult + + A namedtuple which represents the result returned from :meth:`~Guild.bulk_ban`. + + .. versionadded:: 2.4 + + .. attribute:: banned + + The list of users that were banned. The type of the list is a :class:`Object` + representing the user. + + :type: List[:class:`Object`] + .. attribute:: failed + + The list of users that could not be banned. The type of the list is a :class:`Object` + representing the user. + + :type: List[:class:`Object`] + ScheduledEvent ~~~~~~~~~~~~~~ From 0e016be42ca4f34bb89761261a1c7c12f4cc8c48 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 15 Mar 2024 18:11:52 -0400 Subject: [PATCH 020/354] Clarify some docstrings around BulkBanResponse --- discord/guild.py | 2 +- docs/api.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index e65899a54f63..519eb1ce5888 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3814,7 +3814,7 @@ async def bulk_ban( Parameters ----------- - users: :class:`abc.Snowflake` + users: Iterable[:class:`abc.Snowflake`] The user to ban from their guild. delete_message_seconds: :class:`int` The number of seconds worth of messages to delete from the user diff --git a/docs/api.rst b/docs/api.rst index f436b665b590..9a9341d90d08 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4499,14 +4499,14 @@ Guild .. attribute:: banned - The list of users that were banned. The type of the list is a :class:`Object` - representing the user. + The list of users that were banned. The inner :class:`Object` of the list + has the :attr:`Object.type` set to :class:`User`. :type: List[:class:`Object`] .. attribute:: failed - The list of users that could not be banned. The type of the list is a :class:`Object` - representing the user. + The list of users that could not be banned. The inner :class:`Object` of the list + has the :attr:`Object.type` set to :class:`User`. :type: List[:class:`Object`] From 2f71506169bf690b6af993650f4693902693206c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:10:51 +0530 Subject: [PATCH 021/354] Add view_creator_monetization_analytics permission --- discord/permissions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/permissions.py b/discord/permissions.py index 5ba5ea4af7c2..f18f94a7a11d 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -682,6 +682,14 @@ def moderate_members(self) -> int: """ return 1 << 40 + @flag_value + def view_creator_monetization_analytics(self) -> int: + """:class:`bool`: Returns ``True`` if a user can view role subscription insights. + + .. versionadded:: 2.4 + """ + return 1 << 41 + @flag_value def use_soundboard(self) -> int: """:class:`bool`: Returns ``True`` if a user can use the soundboard. From 425edd2e10b9be3d7799c0df0cd1d43a1a34654e Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:32:45 +0530 Subject: [PATCH 022/354] Improve __repr__ for Webhook and SyncWebhook --- discord/webhook/async_.py | 2 +- discord/webhook/sync.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index e2f37e5e62e5..b74916db8d30 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1155,7 +1155,7 @@ def __init__( self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth def __repr__(self) -> str: - return f'' + return f'' @property def url(self) -> str: diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 7da6ada70818..5fee0e41ec03 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -608,7 +608,7 @@ def __init__( self.session: Session = session def __repr__(self) -> str: - return f'' + return f'' @property def url(self) -> str: From dc6d33c30398364bf247c4b22e7afcba2f1bc5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=8A=E3=83=8B=E3=82=AB?= <101696371+ika2kki@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:33:43 +1000 Subject: [PATCH 023/354] copy over original row position for dynamic items --- discord/ui/view.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index f3eda6a60485..dbe985cd5797 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -615,16 +615,15 @@ async def schedule_dynamic_item_call( view = View.from_message(interaction.message, timeout=None) - base_item_index: Optional[int] = None - for index, child in enumerate(view._children): - if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id: - base_item_index = index - break - - if base_item_index is None: + try: + base_item_index, base_item = next( + (index, child) + for index, child in enumerate(view._children) + if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id + ) + except StopIteration: return - base_item = view._children[base_item_index] try: item = await factory.from_custom_id(interaction, base_item, match) except Exception: @@ -634,6 +633,7 @@ async def schedule_dynamic_item_call( # Swap the item in the view with our new dynamic item view._children[base_item_index] = item item._view = view + item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore try: From b8c29b079080a12858bf1da25136d8b10ddc06d4 Mon Sep 17 00:00:00 2001 From: Imayhaveborkedit Date: Thu, 18 Apr 2024 03:36:18 -0400 Subject: [PATCH 024/354] More voice fixes * More voice fixes * Start socket reader paused and wait for socket creation * Fix issues handling 4014 closures Fixes code not handling disconnects from discord's end properly. The 4014 code is shared between being disconnected and moved, so it has to account for the uncertainty. Also properly stops the voice_client audio player when disconnecting. * Fix sending (dropped) silence packets when not connected --- discord/player.py | 3 +- discord/voice_client.py | 4 +-- discord/voice_state.py | 72 ++++++++++++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/discord/player.py b/discord/player.py index b2158d8faeff..5b2c99dc04d4 100644 --- a/discord/player.py +++ b/discord/player.py @@ -763,7 +763,8 @@ def _do_run(self) -> None: delay = max(0, self.DELAY + (next_time - time.perf_counter())) time.sleep(delay) - self.send_silence() + if client.is_connected(): + self.send_silence() def run(self) -> None: try: diff --git a/discord/voice_client.py b/discord/voice_client.py index e5aa05e86df7..442e5af495c4 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -337,7 +337,7 @@ async def disconnect(self, *, force: bool = False) -> None: Disconnects this voice client from voice. """ self.stop() - await self._connection.disconnect(force=force) + await self._connection.disconnect(force=force, wait=True) self.cleanup() async def move_to(self, channel: Optional[abc.Snowflake], *, timeout: Optional[float] = 30.0) -> None: @@ -567,6 +567,6 @@ def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None: try: self._connection.send_packet(packet) except OSError: - _log.info('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) + _log.debug('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295) diff --git a/discord/voice_state.py b/discord/voice_state.py index 8ea83c6519e6..f4a5f76b37ba 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -81,9 +81,10 @@ class SocketReader(threading.Thread): - def __init__(self, state: VoiceConnectionState) -> None: + def __init__(self, state: VoiceConnectionState, *, start_paused: bool = True) -> None: super().__init__(daemon=True, name=f'voice-socket-reader:{id(self):#x}') self.state: VoiceConnectionState = state + self.start_paused = start_paused self._callbacks: List[SocketReaderCallback] = [] self._running = threading.Event() self._end = threading.Event() @@ -130,6 +131,8 @@ def stop(self) -> None: def run(self) -> None: self._end.clear() self._running.set() + if self.start_paused: + self.pause() try: self._do_run() except Exception: @@ -148,7 +151,10 @@ def _do_run(self) -> None: # Since this socket is a non blocking socket, select has to be used to wait on it for reading. try: readable, _, _ = select.select([self.state.socket], [], [], 30) - except (ValueError, TypeError): + except (ValueError, TypeError, OSError) as e: + _log.debug( + "Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e + ) # The socket is either closed or doesn't exist at the moment continue @@ -305,6 +311,10 @@ async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: _log.debug('Ignoring unexpected voice_state_update event') async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: + previous_token = self.token + previous_server_id = self.server_id + previous_endpoint = self.endpoint + self.token = data['token'] self.server_id = int(data['guild_id']) endpoint = data.get('endpoint') @@ -338,6 +348,10 @@ async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: self.state = ConnectionFlowState.got_voice_server_update elif self.state is not ConnectionFlowState.disconnected: + # eventual consistency + if previous_token == self.token and previous_server_id == self.server_id and previous_token == self.token: + return + _log.debug('Unexpected server update event, attempting to handle') await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update) @@ -422,7 +436,7 @@ async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_ if not self._runner: self._runner = self.voice_client.loop.create_task(self._poll_voice_ws(reconnect), name='Voice websocket poller') - async def disconnect(self, *, force: bool = True, cleanup: bool = True) -> None: + async def disconnect(self, *, force: bool = True, cleanup: bool = True, wait: bool = False) -> None: if not force and not self.is_connected(): return @@ -433,23 +447,26 @@ async def disconnect(self, *, force: bool = True, cleanup: bool = True) -> None: except Exception: _log.debug('Ignoring exception disconnecting from voice', exc_info=True) finally: - self.ip = MISSING - self.port = MISSING self.state = ConnectionFlowState.disconnected self._socket_reader.pause() + # Stop threads before we unlock waiters so they end properly + if cleanup: + self._socket_reader.stop() + self.voice_client.stop() + # Flip the connected event to unlock any waiters self._connected.set() self._connected.clear() - if cleanup: - self._socket_reader.stop() - if self.socket: self.socket.close() + self.ip = MISSING + self.port = MISSING + # Skip this part if disconnect was called from the poll loop task - if self._runner and asyncio.current_task() != self._runner: + if wait and not self._inside_runner(): # Wait for the voice_state_update event confirming the bot left the voice channel. # This prevents a race condition caused by disconnecting and immediately connecting again. # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still @@ -458,7 +475,9 @@ async def disconnect(self, *, force: bool = True, cleanup: bool = True) -> None: async with atimeout(self.timeout): await self._disconnected.wait() except TimeoutError: - _log.debug('Timed out waiting for disconnect confirmation event') + _log.debug('Timed out waiting for voice disconnection confirmation') + except asyncio.CancelledError: + pass if cleanup: self.voice_client.cleanup() @@ -476,23 +495,26 @@ async def soft_disconnect(self, *, with_state: ConnectionFlowState = ConnectionF except Exception: _log.debug('Ignoring exception soft disconnecting from voice', exc_info=True) finally: - self.ip = MISSING - self.port = MISSING self.state = with_state self._socket_reader.pause() if self.socket: self.socket.close() + self.ip = MISSING + self.port = MISSING + async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[float]) -> None: if channel is None: - await self.disconnect() + # This function should only be called externally so its ok to wait for the disconnect. + await self.disconnect(wait=True) return if self.voice_client.channel and channel.id == self.voice_client.channel.id: return previous_state = self.state + # this is only an outgoing ws request # if it fails, nothing happens and nothing changes (besides self.state) await self._move_to(channel) @@ -504,7 +526,6 @@ async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[floa _log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id) if self.state is last_state: _log.debug('Reverting to previous state %s', previous_state.name) - self.state = previous_state def wait(self, timeout: Optional[float] = None) -> bool: @@ -527,6 +548,9 @@ def remove_socket_listener(self, callback: SocketReaderCallback) -> None: _log.debug('Unregistering socket listener callback %s', callback) self._socket_reader.unregister(callback) + def _inside_runner(self) -> bool: + return self._runner is not None and asyncio.current_task() == self._runner + async def _wait_for_state( self, state: ConnectionFlowState, *other_states: ConnectionFlowState, timeout: Optional[float] = None ) -> None: @@ -590,11 +614,21 @@ async def _poll_voice_ws(self, reconnect: bool) -> None: break if exc.code == 4014: + # We were disconnected by discord + # This condition is a race between the main ws event and the voice ws closing + if self._disconnected.is_set(): + _log.info('Disconnected from voice by discord, close code %d.', exc.code) + await self.disconnect() + break + + # We may have been moved to a different channel _log.info('Disconnected from voice by force... potentially reconnecting.') successful = await self._potential_reconnect() if not successful: _log.info('Reconnect was unsuccessful, disconnecting from voice normally...') - await self.disconnect() + # Don't bother to disconnect if already disconnected + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() break else: continue @@ -626,10 +660,16 @@ async def _poll_voice_ws(self, reconnect: bool) -> None: async def _potential_reconnect(self) -> bool: try: await self._wait_for_state( - ConnectionFlowState.got_voice_server_update, ConnectionFlowState.got_both_voice_updates, timeout=self.timeout + ConnectionFlowState.got_voice_server_update, + ConnectionFlowState.got_both_voice_updates, + ConnectionFlowState.disconnected, + timeout=self.timeout, ) except asyncio.TimeoutError: return False + else: + if self.state is ConnectionFlowState.disconnected: + return False previous_ws = self.ws try: From f1a19f2f2167dfd423dba8d84c1b45a597927703 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Thu, 18 Apr 2024 09:36:33 +0200 Subject: [PATCH 025/354] Remove entry that is yet to be released from 2.3.2 changelog --- docs/whats_new.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 5a3bee84dc1e..4f09e0a04c8b 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -23,7 +23,6 @@ Bug Fixes - Fix :attr:`Intents.emoji` and :attr:`Intents.emojis_and_stickers` having swapped alias values (:issue:`9471`). - Fix ``NameError`` when using :meth:`abc.GuildChannel.create_invite` (:issue:`9505`). - Fix crash when disconnecting during the middle of a ``HELLO`` packet when using :class:`AutoShardedClient`. -- Fix overly eager escape behaviour for lists and header markdown in :func:`utils.escape_markdown` (:issue:`9516`). - Fix voice websocket not being closed before being replaced by a new one (:issue:`9518`). - |commands| Fix the wrong :meth:`~ext.commands.HelpCommand.on_help_command_error` being called when ejected from a cog. - |commands| Fix ``=None`` being displayed in :attr:`~ext.commands.Command.signature`. From 5497674ae2c0bd24d03f6f35ddadddf473b61e6f Mon Sep 17 00:00:00 2001 From: Vioshim <63890837+Vioshim@users.noreply.github.com> Date: Thu, 18 Apr 2024 02:38:10 -0500 Subject: [PATCH 026/354] Add support for applied_tags in Webhook.send overloaded methods --- discord/webhook/async_.py | 2 ++ discord/webhook/sync.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index b74916db8d30..767db38cc6a8 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1596,6 +1596,7 @@ async def send( wait: Literal[True], suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, ) -> WebhookMessage: ... @@ -1619,6 +1620,7 @@ async def send( wait: Literal[False] = ..., suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, ) -> None: ... diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 5fee0e41ec03..198cdf53ba40 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -44,7 +44,7 @@ from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError from ..message import Message, MessageFlags from ..http import Route, handle_message_parameters -from ..channel import PartialMessageable +from ..channel import PartialMessageable, ForumTag from .async_ import BaseWebhook, _WebhookState @@ -71,6 +71,7 @@ from ..types.message import ( Message as MessagePayload, ) + from ..types.snowflake import SnowflakeList BE = TypeVar('BE', bound=BaseException) @@ -870,6 +871,7 @@ def send( wait: Literal[True], suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, ) -> SyncWebhookMessage: ... @@ -891,6 +893,7 @@ def send( wait: Literal[False] = ..., suppress_embeds: bool = MISSING, silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, ) -> None: ... @@ -911,6 +914,7 @@ def send( wait: bool = False, suppress_embeds: bool = False, silent: bool = False, + applied_tags: List[ForumTag] = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. @@ -1014,6 +1018,11 @@ def send( if thread_name is not MISSING and thread is not MISSING: raise TypeError('Cannot mix thread_name and thread keyword arguments.') + if applied_tags is MISSING: + applied_tag_ids = MISSING + else: + applied_tag_ids: SnowflakeList = [tag.id for tag in applied_tags] + with handle_message_parameters( content=content, username=username, @@ -1027,6 +1036,7 @@ def send( allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, flags=flags, + applied_tags=applied_tag_ids, ) as params: adapter: WebhookAdapter = _get_webhook_adapter() thread_id: Optional[int] = None From 0362b2fd4ea418a20dca22cec46016bc2b7e64eb Mon Sep 17 00:00:00 2001 From: Sebastian Law Date: Thu, 18 Apr 2024 00:38:31 -0700 Subject: [PATCH 027/354] [docs] fix gateway payload hyperlinks --- discord/raw_models.py | 6 +++--- discord/voice_client.py | 2 +- docs/api.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 556df52451ab..74382b3f871b 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -158,7 +158,7 @@ class RawMessageUpdateEvent(_RawReprMixin): .. versionadded:: 1.7 data: :class:`dict` - The raw data given by the :ddocs:`gateway ` + The raw data given by the :ddocs:`gateway ` cached_message: Optional[:class:`Message`] The cached message, if found in the internal message cache. Represents the message before it is modified by the data in :attr:`RawMessageUpdateEvent.data`. @@ -355,7 +355,7 @@ class RawThreadUpdateEvent(_RawReprMixin): parent_id: :class:`int` The ID of the channel the thread belongs to. data: :class:`dict` - The raw data given by the :ddocs:`gateway ` + The raw data given by the :ddocs:`gateway ` thread: Optional[:class:`discord.Thread`] The thread, if it could be found in the internal cache. """ @@ -414,7 +414,7 @@ class RawThreadMembersUpdate(_RawReprMixin): member_count: :class:`int` The approximate number of members in the thread. This caps at 50. data: :class:`dict` - The raw data given by the :ddocs:`gateway `. + The raw data given by the :ddocs:`gateway `. """ __slots__ = ('thread_id', 'guild_id', 'member_count', 'data') diff --git a/discord/voice_client.py b/discord/voice_client.py index 442e5af495c4..3e1c6a5ff967 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -125,7 +125,7 @@ async def on_voice_server_update(self, data: VoiceServerUpdatePayload, /) -> Non Parameters ------------ data: :class:`dict` - The raw :ddocs:`voice server update payload `. + The raw :ddocs:`voice server update payload `. """ raise NotImplementedError diff --git a/docs/api.rst b/docs/api.rst index 9a9341d90d08..11e5a2af18ba 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1008,7 +1008,7 @@ Messages will return a :class:`Message` object that represents the message before the content was modified. Due to the inherently raw nature of this event, the data parameter coincides with - the raw data given by the :ddocs:`gateway `. + the raw data given by the :ddocs:`gateway `. Since the data payload can be partial, care must be taken when accessing stuff in the dictionary. One example of a common case of partial data is when the ``'content'`` key is inaccessible. This From d853a3f0a7e19d290021434e85f9c4c14089a874 Mon Sep 17 00:00:00 2001 From: Willy <19799671+Willy-C@users.noreply.github.com> Date: Thu, 18 Apr 2024 03:39:09 -0400 Subject: [PATCH 028/354] Document bulk ban user limit and permission --- discord/guild.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 519eb1ce5888..c038188c8bec 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3694,7 +3694,7 @@ async def kick(self, user: Snowflake, *, reason: Optional[str] = None) -> None: Parameters ----------- user: :class:`abc.Snowflake` - The user to kick from their guild. + The user to kick from the guild. reason: Optional[:class:`str`] The reason the user got kicked. @@ -3726,7 +3726,7 @@ async def ban( Parameters ----------- user: :class:`abc.Snowflake` - The user to ban from their guild. + The user to ban from the guild. delete_message_days: :class:`int` The number of days worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 7. @@ -3808,14 +3808,14 @@ async def bulk_ban( The users must meet the :class:`abc.Snowflake` abc. - You must have :attr:`~Permissions.ban_members` to do this. + You must have :attr:`~Permissions.ban_members` and :attr:`~Permissions.manage_guild` to do this. .. versionadded:: 2.4 Parameters ----------- users: Iterable[:class:`abc.Snowflake`] - The user to ban from their guild. + The users to ban from the guild, up to 200 users. delete_message_seconds: :class:`int` The number of seconds worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 604800 (7 days). From 8fd1fd805ae9fcb540b56d34152087a4979b57c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Sun, 21 Apr 2024 19:56:20 +0100 Subject: [PATCH 029/354] Fix AutoModRule.edit handling of AutoModRuleEventType enum --- discord/automod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/automod.py b/discord/automod.py index eabf42806be3..61683c269134 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -518,7 +518,7 @@ async def edit( payload['name'] = name if event_type is not MISSING: - payload['event_type'] = event_type + payload['event_type'] = event_type.value if trigger is not MISSING: trigger_metadata = trigger.to_metadata_dict() From 88f62d85d2bb2d8d5d98f01c14700447e3f61b9f Mon Sep 17 00:00:00 2001 From: Michael H Date: Sat, 4 May 2024 23:20:36 -0400 Subject: [PATCH 030/354] Ensure Client.close() has finished in __aexit__ This wraps the closing behavior in a task. Subsequent callers of .close() now await that same close finishing rather than short circuiting. This prevents a user-called close outside of __aexit__ from not finishing before no longer having a running event loop. --- discord/client.py | 33 +++++++++++++++++++-------------- discord/shard.py | 21 ++++++++++++--------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/discord/client.py b/discord/client.py index b3243ef93bd4..f452ca30a50f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -287,7 +287,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: self._enable_debug_events: bool = options.pop('enable_debug_events', False) self._connection: ConnectionState[Self] = self._get_state(intents=intents, **options) self._connection.shard_count = self.shard_count - self._closed: bool = False + self._closing_task: Optional[asyncio.Task[None]] = None self._ready: asyncio.Event = MISSING self._application: Optional[AppInfo] = None self._connection._get_websocket = self._get_websocket @@ -307,7 +307,10 @@ async def __aexit__( exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: - if not self.is_closed(): + # This avoids double-calling a user-provided .close() + if self._closing_task: + await self._closing_task + else: await self.close() # internals @@ -726,22 +729,24 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self._closed: - return + if self._closing_task: + return await self._closing_task - self._closed = True + async def _close(): + await self._connection.close() - await self._connection.close() + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) - if self.ws is not None and self.ws.open: - await self.ws.close(code=1000) + await self.http.close() - await self.http.close() + if self._ready is not MISSING: + self._ready.clear() - if self._ready is not MISSING: - self._ready.clear() + self.loop = MISSING - self.loop = MISSING + self._closing_task = asyncio.create_task(_close()) + await self._closing_task def clear(self) -> None: """Clears the internal state of the bot. @@ -750,7 +755,7 @@ def clear(self) -> None: and :meth:`is_ready` both return ``False`` along with the bot's internal cache cleared. """ - self._closed = False + self._closing_task = None self._ready.clear() self._connection.clear() self.http.clear() @@ -870,7 +875,7 @@ async def runner(): def is_closed(self) -> bool: """:class:`bool`: Indicates if the websocket connection is closed.""" - return self._closed + return self._closing_task is not None @property def activity(self) -> Optional[ActivityTypes]: diff --git a/discord/shard.py b/discord/shard.py index fc8e3380a642..52155aa24dfe 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -481,18 +481,21 @@ async def close(self) -> None: Closes the connection to Discord. """ - if self.is_closed(): - return + if self._closing_task: + return await self._closing_task + + async def _close(): + await self._connection.close() - self._closed = True - await self._connection.close() + to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()] + if to_close: + await asyncio.wait(to_close) - to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()] - if to_close: - await asyncio.wait(to_close) + await self.http.close() + self.__queue.put_nowait(EventItem(EventType.clean_close, None, None)) - await self.http.close() - self.__queue.put_nowait(EventItem(EventType.clean_close, None, None)) + self._closing_task = asyncio.create_task(_close()) + await self._closing_task async def change_presence( self, From 0b4263e697d3eea73925eb81314e278cb3dde2fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 23:21:35 -0400 Subject: [PATCH 031/354] [Crowdin] Updated translation files Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/locale/ja/LC_MESSAGES/api.po | 3592 +++++++++++------ docs/locale/ja/LC_MESSAGES/discord.po | 2 +- docs/locale/ja/LC_MESSAGES/ext/tasks/index.po | 2 +- docs/locale/ja/LC_MESSAGES/faq.po | 2 +- docs/locale/ja/LC_MESSAGES/intents.po | 2 +- docs/locale/ja/LC_MESSAGES/intro.po | 2 +- docs/locale/ja/LC_MESSAGES/logging.po | 14 +- docs/locale/ja/LC_MESSAGES/migrating.po | 2 +- .../ja/LC_MESSAGES/migrating_to_async.po | 2 +- docs/locale/ja/LC_MESSAGES/migrating_to_v1.po | 2 +- docs/locale/ja/LC_MESSAGES/quickstart.po | 2 +- docs/locale/ja/LC_MESSAGES/sphinx.po | 2 +- .../ja/LC_MESSAGES/version_guarantees.po | 2 +- docs/locale/ja/LC_MESSAGES/whats_new.po | 1814 +++++---- 14 files changed, 3242 insertions(+), 2200 deletions(-) diff --git a/docs/locale/ja/LC_MESSAGES/api.po b/docs/locale/ja/LC_MESSAGES/api.po index c6f40a2ebe50..96a912b31695 100644 --- a/docs/locale/ja/LC_MESSAGES/api.po +++ b/docs/locale/ja/LC_MESSAGES/api.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -309,8 +309,8 @@ msgstr "これが ``__init__`` で渡されなかった場合、データを含 #: ../../../discord/client.py:docstring of discord.Client.application_id:10 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:72 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:82 -#: ../../api.rst:1431 -#: ../../../discord/audit_logs.py:docstring of discord.audit_logs.AuditLogEntry:41 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:7 +#: ../../api.rst:1484 msgid "Optional[:class:`int`]" msgstr "Optional[:class:`int`]" @@ -703,8 +703,8 @@ msgstr ":class:`.abc.GuildChannel` を受け取ったからと言って、その #: ../../../discord/client.py:docstring of discord.client.Client.get_all_channels:0 #: ../../../discord/client.py:docstring of discord.client.Client.get_all_members:0 #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:0 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:0 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:0 -#: ../../../discord/user.py:docstring of discord.abc.Messageable.history:0 msgid "Yields" msgstr "Yieldする値" @@ -750,9 +750,9 @@ msgstr "この関数は **条件を満たす最初のイベント** を返しま #: ../../../discord/client.py:docstring of discord.client.Client.wait_for:22 #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:14 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:6 #: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio.from_probe:7 #: ../../../discord/utils.py:docstring of discord.utils.get:24 -#: ../../../discord/abc.py:docstring of discord.abc.GuildChannel.set_permissions:25 msgid "Examples" msgstr "例" @@ -832,23 +832,24 @@ msgid "This method is an API call. For general usage, consider :attr:`guilds` in msgstr "これはAPIを呼び出します。通常は :attr:`guilds` を代わりに使用してください。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:15 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:7 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:7 #: ../../../discord/user.py:docstring of discord.abc.Messageable.history:7 #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction.users:12 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_members:27 msgid "Usage ::" msgstr "使い方 ::" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:20 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:12 #: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:15 msgid "Flattening into a list ::" msgstr "リストへフラット化 ::" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guilds:25 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:17 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.history:19 #: ../../../discord/user.py:docstring of discord.abc.Messageable.history:19 #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_members:10 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:20 msgid "All parameters are optional." msgstr "すべてのパラメータがオプションです。" @@ -930,9 +931,8 @@ msgid "Whether to include count information in the guild. This fills the :attr:` msgstr "ギルドにカウント情報を含めるかどうか。これを使うことで特権インテントがなくても :attr:`.Guild.approximate_member_count` と :attr:`.Guild.approximate_presence_count` 属性が設定されます。デフォルトは ``True`` です。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guild:28 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:16 -msgid "You do not have access to the guild." -msgstr "ギルドにアクセスする権限がない場合。" +msgid "The guild doesn't exist or you got no access to it." +msgstr "" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_guild:29 msgid "Getting the guild failed." @@ -1216,7 +1216,10 @@ msgid "Invalid Channel ID." msgstr "引数が無効なチャンネル IDである場合。" #: ../../../discord/client.py:docstring of discord.client.Client.fetch_channel:18 +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:14 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:14 #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_channel:14 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:14 msgid "You do not have permission to fetch this channel." msgstr "このチャンネルからメッセージを取得する権限がない場合。" @@ -1278,6 +1281,123 @@ msgstr "要求されたスタンプ。" msgid "Union[:class:`.StandardSticker`, :class:`.GuildSticker`]" msgstr "Union[:class:`.StandardSticker`, :class:`.GuildSticker`]" +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:3 +msgid "Retrieves the bot's available SKUs." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:7 +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:11 +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:39 +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:14 +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:5 +msgid "The application ID could not be found." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:8 +msgid "Retrieving the SKUs failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:10 +msgid "The bot's available SKUs." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_skus:11 +msgid "List[:class:`.SKU`]" +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:3 +msgid "Retrieves a :class:`.Entitlement` with the specified ID." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:7 +msgid "The entitlement's ID to fetch from." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:10 +msgid "An entitlement with this ID does not exist." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:12 +msgid "Fetching the entitlement failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:14 +msgid "The entitlement you requested." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.fetch_entitlement:15 +msgid ":class:`.Entitlement`" +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:1 +msgid "Retrieves an :term:`asynchronous iterator` of the :class:`.Entitlement` that applications has." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:19 +msgid "The number of entitlements to retrieve. If ``None``, it retrieves every entitlement for this application. Note, however, that this would make it a slow operation. Defaults to ``100``." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:22 +msgid "Retrieve entitlements before this date or entitlement. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:26 +msgid "Retrieve entitlements after this date or entitlement. If a datetime is provided, it is recommended to use a UTC aware datetime. If the datetime is naive, it is assumed to be local time." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:30 +msgid "A list of SKUs to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:32 +msgid "The user to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:34 +msgid "The guild to filter by." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:36 +msgid "Whether to exclude ended entitlements. Defaults to ``False``." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:40 +msgid "Fetching the entitlements failed." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:41 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:33 +msgid "Both ``after`` and ``before`` were provided, as Discord does not support this type of pagination." +msgstr "``after`` と ``before`` の両方が渡された場合。Discordはこのタイプのページネーションをサポートしていません。" + +#: ../../../discord/client.py:docstring of discord.client.Client.entitlements:43 +msgid ":class:`.Entitlement` -- The entitlement with the application." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:3 +msgid "Creates a test :class:`.Entitlement` for the application." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:7 +msgid "The SKU to create the entitlement for." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:9 +msgid "The ID of the owner." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:11 +msgid "The type of the owner." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:15 +msgid "The SKU or owner could not be found." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.create_entitlement:16 +msgid "Creating the entitlement failed." +msgstr "" + #: ../../../discord/client.py:docstring of discord.client.Client.fetch_premium_sticker_packs:3 msgid "Retrieves all available premium sticker packs." msgstr "利用可能なプレミアムスタンプパックをすべて取得します。" @@ -1322,6 +1442,32 @@ msgstr "作成されたチャンネル。" msgid ":class:`.DMChannel`" msgstr ":class:`.DMChannel`" +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:1 +msgid "Registers :class:`~discord.ui.DynamicItem` classes for persistent listening." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:3 +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:3 +msgid "This method accepts *class types* rather than instances." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:7 +msgid "The classes of dynamic items to add." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.add_dynamic_items:10 +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:10 +msgid "A class is not a subclass of :class:`~discord.ui.DynamicItem`." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:1 +msgid "Removes :class:`~discord.ui.DynamicItem` classes from persistent listening." +msgstr "" + +#: ../../../discord/client.py:docstring of discord.client.Client.remove_dynamic_items:7 +msgid "The classes of dynamic items to remove." +msgstr "" + #: ../../../discord/client.py:docstring of discord.client.Client.add_view:1 msgid "Registers a :class:`~discord.ui.View` for persistent listening." msgstr ":class:`~discord.ui.View` を永続的にインタラクションを受け取るために登録します。" @@ -1386,6 +1532,15 @@ msgstr "シャードの起動時に利用するshard_idsのオプショナルな msgid "Optional[List[:class:`int`]]" msgstr "Optional[List[:class:`int`]]" +#: ../../../discord/shard.py:docstring of discord.shard.AutoShardedClient:37 +msgid "The maximum number of seconds to wait before timing out when launching a shard. Defaults to 180 seconds." +msgstr "" + +#: ../../../discord/shard.py:docstring of discord.shard.AutoShardedClient:42 +#: ../../../discord/message.py:docstring of discord.message.Attachment:99 +msgid "Optional[:class:`float`]" +msgstr "Optional[:class:`float`]" + #: ../../../discord/shard.py:docstring of discord.AutoShardedClient.latency:3 msgid "This operates similarly to :meth:`Client.latency` except it uses the average latency of every shard's latency. To get a list of shard latency, check the :attr:`latencies` property. Returns ``nan`` if there are no shards ready." msgstr "これは :meth:`Client.latency` と同様に機能しますが、すべてのシャードの平均待ち時間を使用する点が異なります。シャードの待ち時間のリストを取得するには :attr:`latencies` プロパティを参照してください。準備ができていない場合は ``nan`` を返します。" @@ -1490,7 +1645,7 @@ msgid "The application owner." msgstr "アプリケーションの所有者。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:20 -#: ../../api.rst:4277 +#: ../../api.rst:4492 #: ../../../discord/integrations.py:docstring of discord.integrations.Integration:45 #: ../../../discord/integrations.py:docstring of discord.integrations.BotIntegration:39 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration:63 @@ -1558,7 +1713,7 @@ msgstr "このアプリケーションがDiscord上で販売されているゲ #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:99 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:107 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:140 -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:40 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:149 msgid "Optional[:class:`str`]" msgstr "Optional[:class:`str`]" @@ -1578,9 +1733,9 @@ msgstr "アプリケーションの機能を説明するタグのリスト。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:115 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:123 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:157 #: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:62 #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInstallParams:10 -#: ../../../discord/guild.py:docstring of discord.guild.Guild:133 msgid "List[:class:`str`]" msgstr "List[:class:`str`]" @@ -1601,6 +1756,16 @@ msgstr "Optional[:class:`AppInstallParams`]" msgid "The application's connection verification URL which will render the application as a verification method in the guild's role verification configuration." msgstr "アプリケーションをギルドのロール紐づけ設定にて紐づけ方法として扱うようにするための、アプリケーションの接続確認URL。" +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:144 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:66 +msgid "The interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway, if configured." +msgstr "設定されている場合、ゲートウェイではなくエンドポイントからインタラクションを受け取るアプリケーションの、インタラクションエンドポイントのURI。" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo:153 +#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:58 +msgid "A list of authentication redirect URIs." +msgstr "認証リダイレクトURIのリスト。" + #: ../../../discord/appinfo.py:docstring of discord.AppInfo.icon:1 #: ../../../discord/appinfo.py:docstring of discord.PartialAppInfo.icon:1 msgid "Retrieves the application's icon asset, if any." @@ -1645,6 +1810,74 @@ msgstr "アプリケーションのフラグ。" msgid ":class:`ApplicationFlags`" msgstr ":class:`ApplicationFlags`" +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:3 +msgid "Edits the application info." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:7 +msgid "The new custom authorization URL for the application. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:9 +msgid "The new application description. Can be ``None`` to remove the description." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:11 +msgid "The new application’s connection verification URL which will render the application as a verification method in the guild’s role verification configuration. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:14 +msgid "The new list of :ddocs:`OAuth2 scopes ` of the :attr:`~install_params`. Can be ``None`` to remove the scopes." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:17 +msgid "The new permissions of the :attr:`~install_params`. Can be ``None`` to remove the permissions." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:19 +msgid "The new application’s flags. Only limited intent flags (:attr:`~ApplicationFlags.gateway_presence_limited`, :attr:`~ApplicationFlags.gateway_guild_members_limited`, :attr:`~ApplicationFlags.gateway_message_content_limited`) can be edited. Can be ``None`` to remove the flags." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:25 +msgid "Editing the limited intent flags leads to the termination of the bot." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:27 +msgid "The new application’s icon as a :term:`py:bytes-like object`. Can be ``None`` to remove the icon." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:29 +msgid "The new application’s cover image as a :term:`py:bytes-like object` on a store embed. The cover image is only available if the application is a game sold on Discord. Can be ``None`` to remove the image." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:33 +msgid "The new interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway. Can be ``None`` to remove the URL." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:36 +msgid "The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:38 +msgid "The reason for editing the application. Shows up on the audit log." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:41 +msgid "Editing the application failed" +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:42 +msgid "The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:44 +msgid "The newly updated application info." +msgstr "" + +#: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInfo.edit:45 +msgid ":class:`AppInfo`" +msgstr "" + #: ../../api.rst:68 msgid "PartialAppInfo" msgstr "PartialAppInfo" @@ -1657,14 +1890,6 @@ msgstr ":func:`~discord.abc.GuildChannel.create_invite` により与えられた msgid "The approximate count of the guilds the bot was added to." msgstr "ボットが追加されたギルドのおおよその数。" -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:58 -msgid "A list of authentication redirect URIs." -msgstr "認証リダイレクトURIのリスト。" - -#: ../../../discord/appinfo.py:docstring of discord.appinfo.PartialAppInfo:66 -msgid "The interactions endpoint url of the application to receive interactions over this endpoint rather than over the gateway, if configured." -msgstr "設定されている場合、ゲートウェイではなくエンドポイントからインタラクションを受け取るアプリケーションの、インタラクションエンドポイントのURI。" - #: ../../../discord/appinfo.py:docstring of discord.PartialAppInfo.cover_image:1 msgid "Retrieves the cover image of the application's default rich presence." msgstr "存在する場合は、アプリケーションの既定のリッチプレゼンスのカバー画像を取得します。" @@ -1686,8 +1911,8 @@ msgid "The permissions to give to application in the guild." msgstr "ギルドに追加するアプリケーションに与える権限。" #: ../../../discord/appinfo.py:docstring of discord.appinfo.AppInstallParams:16 -#: ../../api.rst:3659 -#: ../../api.rst:3748 +#: ../../api.rst:3868 +#: ../../api.rst:3957 #: ../../../discord/member.py:docstring of discord.Member.guild_permissions:14 #: ../../../discord/role.py:docstring of discord.Role.permissions:3 msgid ":class:`Permissions`" @@ -1795,6 +2020,14 @@ msgstr "メンバーの参加状態 (例:招待されたか、承認された msgid ":class:`TeamMembershipState`" msgstr ":class:`TeamMembershipState`" +#: ../../../discord/team.py:docstring of discord.team.TeamMember:69 +msgid "The role of the member within the team." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.team.TeamMember:73 +msgid ":class:`TeamMemberRole`" +msgstr "" + #: ../../../discord/team.py:docstring of discord.TeamMember.accent_color:1 #: ../../../discord/user.py:docstring of discord.ClientUser.accent_color:1 #: ../../../discord/user.py:docstring of discord.User.accent_color:1 @@ -1866,13 +2099,35 @@ msgid "If the user has not uploaded a global avatar, ``None`` is returned. If yo msgstr "ユーザーがグローバルのアバターをアップロードしていない場合は、 ``None`` が返されます。ユーザーが表示しているアバターを取得したい場合は、 :attr:`display_avatar` を検討してください。" #: ../../../discord/team.py:docstring of discord.TeamMember.avatar:6 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:7 #: ../../../discord/team.py:docstring of discord.TeamMember.banner:9 #: ../../../discord/webhook/async_.py:docstring of discord.Webhook.avatar:6 #: ../../../discord/webhook/sync.py:docstring of discord.SyncWebhook.avatar:6 -#: ../../../discord/user.py:docstring of discord.ClientUser.avatar:6 msgid "Optional[:class:`Asset`]" msgstr "Optional[:class:`Asset`]" +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:1 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration:1 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration:1 +#: ../../../discord/widget.py:docstring of discord.WidgetMember.avatar_decoration:1 +msgid "Returns an :class:`Asset` for the avatar decoration the user has." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration:3 +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:3 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration:3 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration_sku_id:3 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration:3 +msgid "If the user has not set an avatar decoration, ``None`` is returned." +msgstr "" + +#: ../../../discord/team.py:docstring of discord.TeamMember.avatar_decoration_sku_id:1 +#: ../../../discord/user.py:docstring of discord.ClientUser.avatar_decoration_sku_id:1 +#: ../../../discord/user.py:docstring of discord.User.avatar_decoration_sku_id:1 +#: ../../../discord/widget.py:docstring of discord.WidgetMember.avatar_decoration_sku_id:1 +msgid "Returns the SKU ID of the avatar decoration the user has." +msgstr "" + #: ../../../discord/team.py:docstring of discord.TeamMember.banner:1 #: ../../../discord/user.py:docstring of discord.ClientUser.banner:1 #: ../../../discord/user.py:docstring of discord.User.banner:1 @@ -1897,7 +2152,7 @@ msgstr ":attr:`colour` という名前のエイリアスが存在します。" #: ../../../discord/team.py:docstring of discord.TeamMember.color:6 #: ../../../discord/team.py:docstring of discord.TeamMember.colour:6 -#: ../../api.rst:3668 +#: ../../api.rst:3877 #: ../../../discord/user.py:docstring of discord.ClientUser.color:6 #: ../../../discord/user.py:docstring of discord.ClientUser.colour:6 msgid ":class:`Colour`" @@ -1950,9 +2205,9 @@ msgstr "ユーザーの既定のアバターを返します。" #: ../../../discord/team.py:docstring of discord.TeamMember.default_avatar:3 #: ../../../discord/team.py:docstring of discord.TeamMember.display_avatar:7 -#: ../../api.rst:3442 -#: ../../api.rst:3448 -#: ../../api.rst:3454 +#: ../../api.rst:3651 +#: ../../api.rst:3657 +#: ../../api.rst:3663 msgid ":class:`Asset`" msgstr ":class:`Asset`" @@ -2079,7 +2334,7 @@ msgstr "接続しているギルド。" #: ../../../discord/voice_client.py:docstring of discord.VoiceClient.guild:3 #: ../../../discord/audit_logs.py:docstring of discord.audit_logs.AuditLogEntry:53 -#: ../../api.rst:3436 +#: ../../api.rst:3645 #: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:15 #: ../../../discord/automod.py:docstring of discord.AutoModAction.guild:3 msgid ":class:`Guild`" @@ -2090,7 +2345,7 @@ msgid "The user connected to voice (i.e. ourselves)." msgstr "ボイスチャンネルに接続しているユーザー。(つまり、自分自身)" #: ../../../discord/voice_client.py:docstring of discord.VoiceClient.user:3 -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:31 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:37 #: ../../../discord/channel.py:docstring of discord.channel.DMChannel:33 #: ../../../discord/channel.py:docstring of discord.channel.GroupChannel:31 msgid ":class:`ClientUser`" @@ -2120,6 +2375,14 @@ msgstr "別のボイスチャンネルへ移動させます。" msgid "The channel to move to. Must be a voice channel." msgstr "移動先のチャンネル。ボイスチャンネルである必要があります。" +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.move_to:7 +msgid "How long to wait for the move to complete." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.move_to:12 +msgid "The move did not complete in time, but may still be ongoing." +msgstr "" + #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.is_connected:1 msgid "Indicates if the voice client is connected to voice." msgstr "ボイスチャンネルに接続しているかどうか。" @@ -2137,29 +2400,65 @@ msgid "If an error happens while the audio player is running, the exception is c msgstr "オーディオプレーヤーの実行中にエラーが発生した場合、例外が捕捉され、オーディオプレーヤーが停止します。 コールバックが渡されない場合、捕捉された例外はライブラリロガーを用いて記録されます。" #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:10 +msgid "Extra parameters may be passed to the internal opus encoder if a PCM based source is used. Otherwise, they are ignored." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:13 msgid "Instead of writing to ``sys.stderr``, the library's logger is used." msgstr "``sys.stderr`` に出力するのではなく、ライブラリロガーが使用されるようになりました。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:13 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:16 +msgid "Added encoder parameters as keyword arguments." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:19 msgid "The audio source we're reading from." msgstr "読み込むオーディオソース。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:15 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:21 msgid "The finalizer that is called after the stream is exhausted. This function must have a single parameter, ``error``, that denotes an optional exception that was raised during playing." msgstr "ファイナライザーはストリームが空になると呼び出されます。この関数には再生中に発生したオプションの例外を表す一つのパラメータ ``error`` が必要です。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:20 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:25 +msgid "Configures the encoder's intended application. Can be one of: ``'audio'``, ``'voip'``, ``'lowdelay'``. Defaults to ``'audio'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:29 +msgid "Configures the bitrate in the encoder. Can be between ``16`` and ``512``. Defaults to ``128``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:32 +msgid "Configures the encoder's use of inband forward error correction. Defaults to ``True``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:35 +msgid "Configures the encoder's expected packet loss percentage. Requires FEC. Defaults to ``0.15``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:38 +msgid "Configures the encoder's bandpass. Can be one of: ``'narrow'``, ``'medium'``, ``'wide'``, ``'superwide'``, ``'full'``. Defaults to ``'full'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:42 +msgid "Configures the type of signal being encoded. Can be one of: ``'auto'``, ``'voice'``, ``'music'``. Defaults to ``'auto'``." +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:47 msgid "Already playing audio or not connected." msgstr "既にオーディオを再生しているか、接続されていない場合。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:21 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:48 msgid "Source is not a :class:`AudioSource` or after is not a callable." msgstr "ソースが :class:`AudioSource` でないか、afterが呼び出し可能でない場合。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:22 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:49 msgid "Source is not opus encoded and opus is not loaded." msgstr "ソースがopusエンコードされておらず、opusが読み込まれていない場合。" +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.play:50 +msgid "An improper value was passed as an encoder parameter." +msgstr "" + #: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceClient.is_playing:1 msgid "Indicates if we're currently playing audio." msgstr "現在オーディオを再生しているか。" @@ -2248,7 +2547,11 @@ msgstr "接続されているボイスチャンネル。" msgid "An abstract method that is called when the client's voice state has changed. This corresponds to ``VOICE_STATE_UPDATE``." msgstr "クライアントの音声状態が変更された際に呼び出される抽象メソッドです。これは ``VOICE_STATE_UPDATE`` と対応しています。" -#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:6 +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:8 +msgid "This method is not the same as the event. See: :func:`on_voice_state_update`" +msgstr "" + +#: ../../../discord/voice_client.py:docstring of discord.voice_client.VoiceProtocol.on_voice_state_update:10 msgid "The raw :ddocs:`voice state payload `." msgstr "生の :ddocs:`ボイスステートペイロード ` 。" @@ -2457,28 +2760,33 @@ msgstr "ffmpegが受け取り、PCMバイトへ変換する入力。 ``pipe`` msgid "The executable name (and path) to use. Defaults to ``ffmpeg``." msgstr "使用する実行可能ファイルの名前 (およびパス)。デフォルトでは ``ffmpeg`` です。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:16 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:41 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:18 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:43 +msgid "Since this class spawns a subprocess, care should be taken to not pass in an arbitrary executable name when using this parameter." +msgstr "" + +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:21 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:46 msgid "If ``True``, denotes that ``source`` parameter will be passed to the stdin of ffmpeg. Defaults to ``False``." msgstr "``True`` の場合、 ``source`` パラメータがffmpegの標準入力に渡されます。デフォルトでは ``False`` です。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:19 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:44 -msgid "A file-like object to pass to the Popen constructor. Could also be an instance of ``subprocess.PIPE``." -msgstr "Popenのコンストラクタに渡すファイルライクオブジェクト。 ``subprocess.PIPE`` のようなインスタンスにすることも可能です。" +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:24 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:49 +msgid "A file-like object to pass to the Popen constructor." +msgstr "" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:22 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:47 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:26 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:51 msgid "Extra command line arguments to pass to ffmpeg before the ``-i`` flag." msgstr "``-i`` フラグのまえにffmepgに渡す追加のコマンドライン引数。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:24 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:49 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:28 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:53 msgid "Extra command line arguments to pass to ffmpeg after the ``-i`` flag." msgstr "``-i`` フラグのあとにffmepgに渡す追加のコマンドライン引数。" -#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:27 -#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:52 +#: ../../../discord/player.py:docstring of discord.player.FFmpegPCMAudio:31 +#: ../../../discord/player.py:docstring of discord.player.FFmpegOpusAudio:56 msgid "The subprocess failed to be created." msgstr "サブプロセスを作成できなかった場合。" @@ -2690,9 +2998,9 @@ msgstr "アプリケーションコマンドの権限が更新されたときに #: ../../api.rst:215 #: ../../api.rst:370 -#: ../../api.rst:736 -#: ../../api.rst:794 -#: ../../api.rst:979 +#: ../../api.rst:777 +#: ../../api.rst:835 +#: ../../api.rst:1020 msgid "The raw event payload data." msgstr "生のイベントペイロードデータ。" @@ -2709,7 +3017,7 @@ msgid "The command that completed successfully" msgstr "正常に実行されたコマンド。" #: ../../api.rst:231 -#: ../../api.rst:4201 +#: ../../api.rst:4416 msgid "AutoMod" msgstr "AutoMod" @@ -2770,8 +3078,8 @@ msgstr "ギルドは :attr:`~abc.GuildChannel.guild` で取得できます。" #: ../../api.rst:291 #: ../../api.rst:300 #: ../../api.rst:311 -#: ../../api.rst:546 -#: ../../api.rst:555 +#: ../../api.rst:587 +#: ../../api.rst:596 msgid "This requires :attr:`Intents.guilds` to be enabled." msgstr ":attr:`Intents.guilds` を有効にする必要があります。" @@ -2809,10 +3117,10 @@ msgid "Called whenever a private group DM is updated. e.g. changed name or topic msgstr "プライベートグループDMが更新されたとき呼び出されます。 例: 名前やトピックの変更。" #: ../../api.rst:322 -#: ../../api.rst:885 -#: ../../api.rst:919 -#: ../../api.rst:936 -#: ../../api.rst:953 +#: ../../api.rst:926 +#: ../../api.rst:960 +#: ../../api.rst:977 +#: ../../api.rst:994 msgid "This requires :attr:`Intents.messages` to be enabled." msgstr ":attr:`Intents.messages` を有効にする必要があります。" @@ -2989,191 +3297,231 @@ msgid "The message that is about to be passed on to the WebSocket library. It ca msgstr "WebSocketライブラリから渡されるメッセージ。バイナリメッセージの場合は :class:`bytes` 、通常のメッセージの場合は :class:`str` です。" #: ../../api.rst:500 +msgid "Entitlements" +msgstr "" + +#: ../../api.rst:504 +msgid "Called when a user subscribes to a SKU." +msgstr "" + +#: ../../api.rst:508 +msgid "The entitlement that was created." +msgstr "" + +#: ../../api.rst:513 +msgid "Called when a user updates their subscription to a SKU. This is usually called when the user renews or cancels their subscription." +msgstr "" + +#: ../../api.rst:518 +msgid "The entitlement that was updated." +msgstr "" + +#: ../../api.rst:523 +msgid "Called when a users subscription to a SKU is cancelled. This is typically only called when:" +msgstr "" + +#: ../../api.rst:525 +msgid "Discord issues a refund for the subscription." +msgstr "" + +#: ../../api.rst:526 +msgid "Discord removes an entitlement from a user." +msgstr "" + +#: ../../api.rst:530 +msgid "This event won't be called if the user cancels their subscription manually, instead :func:`on_entitlement_update` will be called with :attr:`Entitlement.ends_at` set to the end of the current billing period." +msgstr "" + +#: ../../api.rst:536 +msgid "The entitlement that was deleted." +msgstr "" + +#: ../../api.rst:541 msgid "Gateway" msgstr "Gateway" -#: ../../api.rst:504 +#: ../../api.rst:545 msgid "Called when the client is done preparing the data received from Discord. Usually after login is successful and the :attr:`Client.guilds` and co. are filled up." msgstr "クライアントがDiscordから受信したデータの準備を完了した際に呼び出されます。通常はログインが成功したあと、 :attr:`Client.guilds` とそれに関連するものの準備が完了したときです。" -#: ../../api.rst:509 +#: ../../api.rst:550 msgid "This function is not guaranteed to be the first event called. Likewise, this function is **not** guaranteed to only be called once. This library implements reconnection logic and thus will end up calling this event whenever a RESUME request fails." msgstr "このイベントは、最初に呼び出されるイベントとは限りません。同時に、このイベントは **一度だけ呼ばれるという保証もできません** 。このライブラリは、再接続ロジックを実装しているためリジューム要求が失敗するたびにこのイベントが呼び出されることになります。" -#: ../../api.rst:516 +#: ../../api.rst:557 msgid "Called when the client has resumed a session." msgstr "クライアントがセッションを再開したときに呼び出されます。" -#: ../../api.rst:520 +#: ../../api.rst:561 msgid "Similar to :func:`on_ready` except used by :class:`AutoShardedClient` to denote when a particular shard ID has become ready." msgstr "特定の Shard IDが準備完了になったかを確認するために :class:`AutoShardedClient` で使用される以外は :func:`on_ready` とほとんど同じです。" -#: ../../api.rst:523 +#: ../../api.rst:564 msgid "The shard ID that is ready." msgstr "準備が完了したShard ID。" -#: ../../api.rst:529 +#: ../../api.rst:570 msgid "Similar to :func:`on_resumed` except used by :class:`AutoShardedClient` to denote when a particular shard ID has resumed a session." msgstr "特定のシャードIDを持つシャードがセッションを再開したかどうかを確認するために :class:`AutoShardedClient` で使用されることを除けば :func:`on_resumed` とほとんど同じです。" -#: ../../api.rst:534 +#: ../../api.rst:575 msgid "The shard ID that has resumed." msgstr "セッションが再開したシャードのID。" -#: ../../api.rst:538 +#: ../../api.rst:579 msgid "Guilds" msgstr "Guilds" -#: ../../api.rst:543 +#: ../../api.rst:584 msgid "Called when a guild becomes available or unavailable. The guild must have existed in the :attr:`Client.guilds` cache." msgstr "ギルドが利用可能・不可能になったときに呼び出されます。ギルドは :attr:`Client.guilds` キャッシュに存在していないといけません。" -#: ../../api.rst:548 +#: ../../api.rst:589 msgid "The :class:`Guild` that has changed availability." msgstr "利用状況が変わった :class:`Guild` 。" -#: ../../api.rst:552 +#: ../../api.rst:593 msgid "Called when a :class:`Guild` is either created by the :class:`Client` or when the :class:`Client` joins a guild." msgstr ":class:`Client` によって :class:`Guild` が作成された。または :class:`Client` がギルドに参加したときに呼び出されます。" -#: ../../api.rst:557 +#: ../../api.rst:598 msgid "The guild that was joined." msgstr "参加したギルド。" -#: ../../api.rst:562 +#: ../../api.rst:603 msgid "Called when a :class:`Guild` is removed from the :class:`Client`." msgstr ":class:`Client` が :class:`Guild` から削除されたときに呼び出されます。" -#: ../../api.rst:564 +#: ../../api.rst:605 msgid "This happens through, but not limited to, these circumstances:" msgstr "これは以下の状況時に呼び出されますが、これに限ったものではありません:" -#: ../../api.rst:566 +#: ../../api.rst:607 msgid "The client got banned." msgstr "クライアントがBANされた。" -#: ../../api.rst:567 +#: ../../api.rst:608 msgid "The client got kicked." msgstr "クライアントがキックされた。" -#: ../../api.rst:568 +#: ../../api.rst:609 msgid "The client left the guild." msgstr "クライアントがギルドから脱退した。" -#: ../../api.rst:569 +#: ../../api.rst:610 msgid "The client or the guild owner deleted the guild." msgstr "クライアント、またはギルドオーナーがギルドを削除した。" -#: ../../api.rst:571 +#: ../../api.rst:612 msgid "In order for this event to be invoked then the :class:`Client` must have been part of the guild to begin with. (i.e. it is part of :attr:`Client.guilds`)" msgstr "このイベントが呼び出されるためには、 :class:`Client` がギルドに参加している必要があります。(つまり、 :attr:`Client.guilds` にギルドが存在しなければならない)" -#: ../../api.rst:576 +#: ../../api.rst:617 msgid "The guild that got removed." msgstr "削除されたギルド。" -#: ../../api.rst:581 +#: ../../api.rst:622 msgid "Called when a :class:`Guild` updates, for example:" msgstr ":class:`Guild` が更新されたときに呼び出されます。例えば:" -#: ../../api.rst:583 +#: ../../api.rst:624 msgid "Changed name" msgstr "名前が変更された" -#: ../../api.rst:584 +#: ../../api.rst:625 msgid "Changed AFK channel" msgstr "AFKチャンネルが変更された" -#: ../../api.rst:585 +#: ../../api.rst:626 msgid "Changed AFK timeout" msgstr "AFKのタイムアウト時間が変更された" -#: ../../api.rst:586 +#: ../../api.rst:627 msgid "etc" msgstr "その他" -#: ../../api.rst:590 +#: ../../api.rst:631 msgid "The guild prior to being updated." msgstr "更新される前のギルド。" -#: ../../api.rst:592 +#: ../../api.rst:633 msgid "The guild after being updated." msgstr "更新された後のギルド。" -#: ../../api.rst:597 +#: ../../api.rst:638 msgid "Called when a :class:`Guild` adds or removes :class:`Emoji`." msgstr ":class:`Guild` に :class:`Emoji` が追加、または削除されたときに呼び出されます。" -#: ../../api.rst:599 -#: ../../api.rst:612 +#: ../../api.rst:640 +#: ../../api.rst:653 msgid "This requires :attr:`Intents.emojis_and_stickers` to be enabled." msgstr ":attr:`Intents.emojis_and_stickers` を有効にする必要があります。" -#: ../../api.rst:601 +#: ../../api.rst:642 msgid "The guild who got their emojis updated." msgstr "絵文字が更新されたギルド。" -#: ../../api.rst:603 +#: ../../api.rst:644 msgid "A list of emojis before the update." msgstr "更新前の絵文字のリスト。" -#: ../../api.rst:605 +#: ../../api.rst:646 msgid "A list of emojis after the update." msgstr "更新後の絵文字のリスト。" -#: ../../api.rst:610 +#: ../../api.rst:651 msgid "Called when a :class:`Guild` updates its stickers." msgstr ":class:`Guild` のスタンプが更新されたときに呼び出されます。" -#: ../../api.rst:616 +#: ../../api.rst:657 msgid "The guild who got their stickers updated." msgstr "スタンプが更新されたギルド。" -#: ../../api.rst:618 +#: ../../api.rst:659 msgid "A list of stickers before the update." msgstr "更新前のスタンプのリスト。" -#: ../../api.rst:620 +#: ../../api.rst:661 msgid "A list of stickers after the update." msgstr "更新後のスタンプのリスト。" -#: ../../api.rst:625 +#: ../../api.rst:666 msgid "Called when a :class:`Guild` gets a new audit log entry. You must have :attr:`~Permissions.view_audit_log` to receive this." msgstr ":class:`Guild` に新しい監査ログ項目が追加されたときに呼び出されます。これを受け取るには :attr:`~Permissions.view_audit_log` が必要です。" -#: ../../api.rst:628 -#: ../../api.rst:840 -#: ../../api.rst:853 +#: ../../api.rst:669 +#: ../../api.rst:881 +#: ../../api.rst:894 msgid "This requires :attr:`Intents.moderation` to be enabled." msgstr ":attr:`Intents.moderation` を有効にする必要があります。" -#: ../../api.rst:634 +#: ../../api.rst:675 msgid "Audit log entries received through the gateway are subject to data retrieval from cache rather than REST. This means that some data might not be present when you expect it to be. For example, the :attr:`AuditLogEntry.target` attribute will usually be a :class:`discord.Object` and the :attr:`AuditLogEntry.user` attribute will depend on user and member cache." msgstr "ゲートウェイ経由で取得した監査ログ項目はデータをRESTではなくキャッシュから取得します。このため、一部のデータが不足している場合があります。例えば、 :attr:`AuditLogEntry.target` 属性は多くの場合 :class:`discord.Object` になり、 :attr:`AuditLogEntry.user` 属性はユーザーとメンバーキャッシュに依存します。" -#: ../../api.rst:640 +#: ../../api.rst:681 msgid "To get the user ID of entry, :attr:`AuditLogEntry.user_id` can be used instead." msgstr "項目のユーザーIDを取得するには、代わりに :attr:`AuditLogEntry.user_id` を使用できます。" -#: ../../api.rst:642 +#: ../../api.rst:683 msgid "The audit log entry that was created." msgstr "作成された監査ログの項目。" -#: ../../api.rst:647 +#: ../../api.rst:688 msgid "Called when an :class:`Invite` is created. You must have :attr:`~Permissions.manage_channels` to receive this." msgstr ":class:`Invite` が作成されたときに呼び出されます。 受け取るには :attr:`~Permissions.manage_channels` が必要です。" -#: ../../api.rst:654 -#: ../../api.rst:671 +#: ../../api.rst:695 +#: ../../api.rst:712 msgid "There is a rare possibility that the :attr:`Invite.guild` and :attr:`Invite.channel` attributes will be of :class:`Object` rather than the respective models." msgstr "まれに :attr:`Invite.guild` と :attr:`Invite.channel` 属性がそれぞれの本来のモデルではなく :class:`Object` になることがあります。" -#: ../../api.rst:657 -#: ../../api.rst:677 +#: ../../api.rst:698 +#: ../../api.rst:718 msgid "This requires :attr:`Intents.invites` to be enabled." msgstr ":attr:`Intents.invites` を有効にする必要があります。" -#: ../../api.rst:659 +#: ../../api.rst:700 #: ../../../discord/abc.py:docstring of discord.abc.GuildChannel.create_invite:37 #: ../../../discord/channel.py:docstring of discord.abc.GuildChannel.create_invite:37 #: ../../../discord/channel.py:docstring of discord.abc.GuildChannel.create_invite:37 @@ -3181,704 +3529,712 @@ msgstr ":attr:`Intents.invites` を有効にする必要があります。" msgid "The invite that was created." msgstr "作成された招待。" -#: ../../api.rst:664 +#: ../../api.rst:705 msgid "Called when an :class:`Invite` is deleted. You must have :attr:`~Permissions.manage_channels` to receive this." msgstr ":class:`Invite` が削除されたときに呼び出されます。 受け取るには :attr:`~Permissions.manage_channels` が必要です。" -#: ../../api.rst:674 +#: ../../api.rst:715 msgid "Outside of those two attributes, the only other attribute guaranteed to be filled by the Discord gateway for this event is :attr:`Invite.code`." msgstr "これらの属性以外では、Discordゲートウェイによってこのイベントに与えられているのが保証されている属性は :attr:`Invite.code` のみです。" -#: ../../api.rst:679 +#: ../../api.rst:720 msgid "The invite that was deleted." msgstr "削除された招待。" -#: ../../api.rst:684 +#: ../../api.rst:725 msgid "Integrations" msgstr "Integrations" -#: ../../api.rst:688 +#: ../../api.rst:729 msgid "Called when an integration is created." msgstr "連携サービスが作成されたときに呼び出されます。" -#: ../../api.rst:690 -#: ../../api.rst:701 -#: ../../api.rst:712 -#: ../../api.rst:732 +#: ../../api.rst:731 +#: ../../api.rst:742 +#: ../../api.rst:753 +#: ../../api.rst:773 msgid "This requires :attr:`Intents.integrations` to be enabled." msgstr ":attr:`Intents.integrations` を有効にする必要があります。" -#: ../../api.rst:694 +#: ../../api.rst:735 msgid "The integration that was created." msgstr "作成された連携サービス。" -#: ../../api.rst:699 +#: ../../api.rst:740 msgid "Called when an integration is updated." msgstr "連携サービスが更新されたときに呼び出されます。" -#: ../../api.rst:705 +#: ../../api.rst:746 msgid "The integration that was updated." msgstr "更新された連携サービス。" -#: ../../api.rst:710 +#: ../../api.rst:751 msgid "Called whenever an integration is created, modified, or removed from a guild." msgstr "ギルドの連携サービスが作成、更新、削除されるたびに呼び出されます。" -#: ../../api.rst:716 +#: ../../api.rst:757 msgid "The guild that had its integrations updated." msgstr "連携サービスが更新されたギルド。" -#: ../../api.rst:721 +#: ../../api.rst:762 msgid "Called whenever a webhook is created, modified, or removed from a guild channel." msgstr "ギルドチャンネルのWebhookが作成、更新、削除されたときに呼び出されます。" -#: ../../api.rst:723 +#: ../../api.rst:764 msgid "This requires :attr:`Intents.webhooks` to be enabled." msgstr ":attr:`Intents.webhooks` を有効にする必要があります。" -#: ../../api.rst:725 +#: ../../api.rst:766 msgid "The channel that had its webhooks updated." msgstr "Webhookが更新されたチャンネル。" -#: ../../api.rst:730 +#: ../../api.rst:771 msgid "Called when an integration is deleted." msgstr "連携サービスが削除されたときに呼び出されます。" -#: ../../api.rst:740 +#: ../../api.rst:781 msgid "Interactions" msgstr "Interactions" -#: ../../api.rst:744 +#: ../../api.rst:785 msgid "Called when an interaction happened." msgstr "インタラクションが発生したときに呼び出されます。" -#: ../../api.rst:746 +#: ../../api.rst:787 msgid "This currently happens due to slash command invocations or components being used." msgstr "これは、現在はスラッシュコマンドの呼び出しやコンポーネントの使用により起こります。" -#: ../../api.rst:750 +#: ../../api.rst:791 msgid "This is a low level function that is not generally meant to be used. If you are working with components, consider using the callbacks associated with the :class:`~discord.ui.View` instead as it provides a nicer user experience." msgstr "これは、一般的な使用を意図していない低レベル関数です。コンポーネントを使用している場合は、よりよいユーザーエクスペリエンスを提供する :class:`~discord.ui.View` のコールバックの使用を検討してください。" -#: ../../api.rst:756 +#: ../../api.rst:797 msgid "The interaction data." msgstr "インタラクションデータ。" -#: ../../api.rst:760 +#: ../../api.rst:801 msgid "Members" msgstr "Members" -#: ../../api.rst:764 +#: ../../api.rst:805 msgid "Called when a :class:`Member` joins a :class:`Guild`." msgstr ":class:`Member` が :class:`Guild` に参加したときに呼び出されます。" -#: ../../api.rst:766 -#: ../../api.rst:778 -#: ../../api.rst:790 -#: ../../api.rst:812 -#: ../../api.rst:829 +#: ../../api.rst:807 +#: ../../api.rst:819 +#: ../../api.rst:831 +#: ../../api.rst:853 +#: ../../api.rst:870 msgid "This requires :attr:`Intents.members` to be enabled." msgstr ":attr:`Intents.members` を有効にする必要があります。" -#: ../../api.rst:768 +#: ../../api.rst:809 msgid "The member who joined." msgstr "参加したメンバー。" -#: ../../api.rst:773 -#: ../../api.rst:785 +#: ../../api.rst:814 +#: ../../api.rst:826 msgid "Called when a :class:`Member` leaves a :class:`Guild`." msgstr ":class:`Member` が :class:`Guild` から脱退したときに呼び出されます。" -#: ../../api.rst:775 +#: ../../api.rst:816 msgid "If the guild or member could not be found in the internal cache this event will not be called, you may use :func:`on_raw_member_remove` instead." msgstr "ギルドまたはメンバーが内部キャッシュで見つからない場合、このイベントは呼び出されません。代わりに :func:`on_raw_member_remove` を使用してください。" -#: ../../api.rst:780 +#: ../../api.rst:821 msgid "The member who left." msgstr "脱退したメンバー。" -#: ../../api.rst:787 +#: ../../api.rst:828 msgid "Unlike :func:`on_member_remove` this is called regardless of the guild or member being in the internal cache." msgstr ":func:`on_member_remove` とは異なり、ギルドやメンバーが内部キャッシュに存在するかどうかに関係なく呼び出されます。" -#: ../../api.rst:799 +#: ../../api.rst:840 msgid "Called when a :class:`Member` updates their profile." msgstr ":class:`Member` のプロフィールが更新されたときに呼び出されます。" -#: ../../api.rst:801 -#: ../../api.rst:823 +#: ../../api.rst:842 #: ../../api.rst:864 +#: ../../api.rst:905 msgid "This is called when one or more of the following things change:" msgstr "これらのうちひとつ以上が変更されたとき呼び出されます:" -#: ../../api.rst:803 +#: ../../api.rst:844 msgid "nickname" msgstr "ニックネーム" -#: ../../api.rst:804 +#: ../../api.rst:845 #: ../../../discord/member.py:docstring of discord.member.Member.edit:16 msgid "roles" msgstr "roles" -#: ../../api.rst:805 +#: ../../api.rst:846 msgid "pending" msgstr "ペンディング状態" -#: ../../api.rst:806 +#: ../../api.rst:847 msgid "timeout" msgstr "タイムアウト" -#: ../../api.rst:807 +#: ../../api.rst:848 msgid "guild avatar" msgstr "ギルドアバター" -#: ../../api.rst:808 +#: ../../api.rst:849 msgid "flags" msgstr "flags" -#: ../../api.rst:810 +#: ../../api.rst:851 msgid "Due to a Discord limitation, this event is not dispatched when a member's timeout expires." msgstr "Discordの制限により、このイベントはメンバーのタイムアウト期間が満了した場合には発生しません。" -#: ../../api.rst:814 -#: ../../api.rst:873 +#: ../../api.rst:855 +#: ../../api.rst:914 msgid "The updated member's old info." msgstr "更新されたメンバーの更新前情報。" -#: ../../api.rst:816 -#: ../../api.rst:875 +#: ../../api.rst:857 +#: ../../api.rst:916 msgid "The updated member's updated info." msgstr "更新されたメンバーの更新後情報。" -#: ../../api.rst:821 +#: ../../api.rst:862 msgid "Called when a :class:`User` updates their profile." msgstr ":class:`User` がプロフィールを編集したとき呼び出されます。" -#: ../../api.rst:825 +#: ../../api.rst:866 msgid "avatar" msgstr "アバター" -#: ../../api.rst:826 +#: ../../api.rst:867 msgid "username" msgstr "ユーザー名" -#: ../../api.rst:827 +#: ../../api.rst:868 msgid "discriminator" msgstr "タグ" -#: ../../api.rst:831 +#: ../../api.rst:872 msgid "The updated user's old info." msgstr "更新されたユーザーの更新前情報。" -#: ../../api.rst:833 +#: ../../api.rst:874 msgid "The updated user's updated info." msgstr "更新されたユーザーの更新後情報。" -#: ../../api.rst:838 -msgid "Called when user gets banned from a :class:`Guild`." -msgstr "ユーザーが :class:`Guild` からBANされたとき呼び出されます。" +#: ../../api.rst:879 +msgid "Called when a user gets banned from a :class:`Guild`." +msgstr "" -#: ../../api.rst:842 +#: ../../api.rst:883 msgid "The guild the user got banned from." msgstr "ユーザーがBANされたギルド。" -#: ../../api.rst:844 +#: ../../api.rst:885 msgid "The user that got banned. Can be either :class:`User` or :class:`Member` depending if the user was in the guild or not at the time of removal." msgstr "BANされたユーザー。BAN時にユーザーがギルドにいたかによって、 :class:`User` か :class:`Member` になります。" -#: ../../api.rst:851 +#: ../../api.rst:892 msgid "Called when a :class:`User` gets unbanned from a :class:`Guild`." msgstr ":class:`User` が :class:`Guild` のBANを解除されたとき呼び出されます。" -#: ../../api.rst:855 +#: ../../api.rst:896 msgid "The guild the user got unbanned from." msgstr "ユーザーのBANが解除されたギルド。" -#: ../../api.rst:857 +#: ../../api.rst:898 msgid "The user that got unbanned." msgstr "Banが解除されたユーザー。" -#: ../../api.rst:862 +#: ../../api.rst:903 msgid "Called when a :class:`Member` updates their presence." msgstr ":class:`Member` がプレゼンスを変更したとき呼び出されます。" -#: ../../api.rst:866 +#: ../../api.rst:907 msgid "status" msgstr "ステータス" -#: ../../api.rst:867 +#: ../../api.rst:908 msgid "activity" msgstr "アクティビティ" -#: ../../api.rst:869 +#: ../../api.rst:910 msgid "This requires :attr:`Intents.presences` and :attr:`Intents.members` to be enabled." msgstr "これを使用するには :attr:`Intents.presences` と :attr:`Intents.members` を有効にしないといけません。" -#: ../../api.rst:879 +#: ../../api.rst:920 msgid "Messages" msgstr "Messages" -#: ../../api.rst:883 +#: ../../api.rst:924 msgid "Called when a :class:`Message` is created and sent." msgstr ":class:`Message` が作成され送信されたときに呼び出されます。" -#: ../../api.rst:889 +#: ../../api.rst:930 msgid "Your bot's own messages and private messages are sent through this event. This can lead cases of 'recursion' depending on how your bot was programmed. If you want the bot to not reply to itself, consider checking the user IDs. Note that :class:`~ext.commands.Bot` does not have this problem." msgstr "Botのメッセージとプライベートメッセージはこのイベントを通して送信されます。Botのプログラムによっては「再帰呼び出し」を続けることになります。Botが自分自身に返信しないようにするためにはユーザーIDを確認する方法が考えられます。 :class:`~ext.commands.Bot` にはこの問題は存在しません。" -#: ../../api.rst:895 +#: ../../api.rst:936 msgid "The current message." msgstr "現在のメッセージ。" -#: ../../api.rst:900 +#: ../../api.rst:941 msgid "Called when a :class:`Message` receives an update event. If the message is not found in the internal message cache, then these events will not be called. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr ":class:`Message` が更新イベントを受け取ったときに呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:905 +#: ../../api.rst:946 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_message_edit` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_message_edit` イベントを使用してください。" -#: ../../api.rst:908 +#: ../../api.rst:949 msgid "The following non-exhaustive cases trigger this event:" msgstr "以下の非網羅的ケースがこのイベントを発生させます:" -#: ../../api.rst:910 +#: ../../api.rst:951 msgid "A message has been pinned or unpinned." msgstr "メッセージをピン留め、または解除した。" -#: ../../api.rst:911 +#: ../../api.rst:952 msgid "The message content has been changed." msgstr "メッセージの内容を変更した。" -#: ../../api.rst:912 +#: ../../api.rst:953 msgid "The message has received an embed." msgstr "メッセージが埋め込みを受け取った。" -#: ../../api.rst:914 +#: ../../api.rst:955 msgid "For performance reasons, the embed server does not do this in a \"consistent\" manner." msgstr "パフォーマンス上の理由から、埋め込みのサーバーはこれを「一貫した」方法では行いません。" -#: ../../api.rst:916 +#: ../../api.rst:957 msgid "The message's embeds were suppressed or unsuppressed." msgstr "メッセージの埋め込みが削除されたり、復元されたりした。" -#: ../../api.rst:917 +#: ../../api.rst:958 msgid "A call message has received an update to its participants or ending time." msgstr "通話呼び出しメッセージの参加者や終了時刻が変わった。" -#: ../../api.rst:921 +#: ../../api.rst:962 msgid "The previous version of the message." msgstr "更新前のメッセージ。" -#: ../../api.rst:923 +#: ../../api.rst:964 msgid "The current version of the message." msgstr "更新後のメッセージ。" -#: ../../api.rst:928 +#: ../../api.rst:969 msgid "Called when a message is deleted. If the message is not found in the internal message cache, then this event will not be called. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr "メッセージが削除された際に呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:933 +#: ../../api.rst:974 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_message_delete` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_message_delete` イベントを使用してください。" -#: ../../api.rst:938 +#: ../../api.rst:979 msgid "The deleted message." msgstr "削除されたメッセージ。" -#: ../../api.rst:943 +#: ../../api.rst:984 msgid "Called when messages are bulk deleted. If none of the messages deleted are found in the internal message cache, then this event will not be called. If individual messages were not found in the internal message cache, this event will still be called, but the messages not found will not be included in the messages list. Messages might not be in cache if the message is too old or the client is participating in high traffic guilds." msgstr "メッセージが一括削除されたときに呼び出されます。メッセージが内部のメッセージキャッシュに見つからない場合、このイベントは呼び出されません。個々のメッセージが見つからない場合でも、このイベントは呼び出されますが、見つからなかったメッセージはメッセージのリストに含まれません。これはメッセージが古すぎるか、クライアントが通信の多いギルドに参加している場合に発生します。" -#: ../../api.rst:950 +#: ../../api.rst:991 msgid "If this occurs increase the :class:`max_messages ` parameter or use the :func:`on_raw_bulk_message_delete` event instead." msgstr "発生する場合は :class:`max_messages ` パラメータの値を増加させるか :func:`on_raw_bulk_message_delete` イベントを使用してください。" -#: ../../api.rst:955 +#: ../../api.rst:996 msgid "The messages that have been deleted." msgstr "削除されたメッセージのリスト。" -#: ../../api.rst:960 +#: ../../api.rst:1001 msgid "Called when a message is edited. Unlike :func:`on_message_edit`, this is called regardless of the state of the internal message cache." msgstr "メッセージが編集されたときに呼び出されます。 :func:`on_message_edit` とは異なり、これは内部のメッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:963 +#: ../../api.rst:1004 msgid "If the message is found in the message cache, it can be accessed via :attr:`RawMessageUpdateEvent.cached_message`. The cached message represents the message before it has been edited. For example, if the content of a message is modified and triggers the :func:`on_raw_message_edit` coroutine, the :attr:`RawMessageUpdateEvent.cached_message` will return a :class:`Message` object that represents the message before the content was modified." msgstr "メッセージがメッセージキャッシュに存在した場合、 :attr:`RawMessageUpdateEvent.cached_message` を介してそのメッセージにアクセスすることができます。キャッシュされていたメッセージは編集前のメッセージです。たとえば、メッセージの内容が編集され、 :func:`on_raw_message_edit` が発火された場合、 :attr:`RawMessageUpdateEvent.cached_message` は内容が編集される前の情報を持つ :class:`Message` オブジェクトを返します。" -#: ../../api.rst:969 +#: ../../api.rst:1010 msgid "Due to the inherently raw nature of this event, the data parameter coincides with the raw data given by the :ddocs:`gateway `." msgstr "このイベントの性質は、本質的に生表現のため、データのパラメータは :ddocs:`ゲートウェイ ` によって与えられた生データと一致します。" -#: ../../api.rst:972 +#: ../../api.rst:1013 msgid "Since the data payload can be partial, care must be taken when accessing stuff in the dictionary. One example of a common case of partial data is when the ``'content'`` key is inaccessible. This denotes an \"embed\" only edit, which is an edit in which only the embeds are updated by the Discord embed server." msgstr "データのペイロードが部分的であるため、データにアクセスするときは気をつけてください。部分的なデータの主な場合のひとつは、``'content'`` にアクセスできない場合です。Discordの埋め込みサーバーによって、埋め込みが更新される、\"埋め込み\"しか変わっていない編集がそうです。" -#: ../../api.rst:985 +#: ../../api.rst:1026 msgid "Called when a message is deleted. Unlike :func:`on_message_delete`, this is called regardless of the message being in the internal message cache or not." msgstr "メッセージが削除されたときに呼び出されます。 :func:`on_message_delete` とは異なり、削除されたメッセージが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:988 +#: ../../api.rst:1029 msgid "If the message is found in the message cache, it can be accessed via :attr:`RawMessageDeleteEvent.cached_message`" msgstr "メッセージがメッセージキャッシュ内に見つかった場合、 :attr:`RawMessageDeleteEvent.cached_message` を介してアクセスすることができます。" -#: ../../api.rst:998 +#: ../../api.rst:1039 msgid "Called when a bulk delete is triggered. Unlike :func:`on_bulk_message_delete`, this is called regardless of the messages being in the internal message cache or not." msgstr "メッセージが一括削除されたときに呼び出されます。 :func:`on_bulk_message_delete` とは異なり、削除されたメッセージが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1001 +#: ../../api.rst:1042 msgid "If the messages are found in the message cache, they can be accessed via :attr:`RawBulkMessageDeleteEvent.cached_messages`" msgstr "メッセージがメッセージキャッシュ内に見つかった場合、 :attr:`RawBulkMessageDeleteEvent.cached_messages` を介してアクセスすることができます。" -#: ../../api.rst:1010 +#: ../../api.rst:1051 msgid "Reactions" msgstr "Reactions" -#: ../../api.rst:1014 +#: ../../api.rst:1055 msgid "Called when a message has a reaction added to it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_add` instead." msgstr "メッセージにリアクションが追加されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_add` の利用を検討してください。" -#: ../../api.rst:1020 +#: ../../api.rst:1061 msgid "To get the :class:`Message` being reacted, access it via :attr:`Reaction.message`." msgstr "リアクションの付いた :class:`Message` を取得するには、 :attr:`Reaction.message` を使ってください。" -#: ../../api.rst:1022 -#: ../../api.rst:1065 -#: ../../api.rst:1078 -#: ../../api.rst:1091 -#: ../../api.rst:1101 +#: ../../api.rst:1063 +#: ../../api.rst:1118 +#: ../../api.rst:1131 +#: ../../api.rst:1144 +#: ../../api.rst:1154 msgid "This requires :attr:`Intents.reactions` to be enabled." msgstr ":attr:`Intents.reactions` を有効にする必要があります。" -#: ../../api.rst:1026 +#: ../../api.rst:1067 msgid "This doesn't require :attr:`Intents.members` within a guild context, but due to Discord not providing updated user information in a direct message it's required for direct messages to receive this event. Consider using :func:`on_raw_reaction_add` if you need this and do not otherwise want to enable the members intent." msgstr "ギルド内では :attr:`Intents.members` は有効にしなくてもよいですが、DiscordがDM内では更新されたユーザーの情報を提供しないため、DMではこのインテントが必要です。 このイベントが必要でメンバーインテントを有効化したくない場合は :func:`on_raw_reaction_add` の使用を検討してください。" -#: ../../api.rst:1032 -#: ../../api.rst:1054 +#: ../../api.rst:1075 +msgid "This event does not have a way of differentiating whether a reaction is a burst reaction (also known as \"super reaction\") or not. If you need this, consider using :func:`on_raw_reaction_add` instead." +msgstr "" + +#: ../../api.rst:1079 +#: ../../api.rst:1107 msgid "The current state of the reaction." msgstr "リアクションの現在の状態。" -#: ../../api.rst:1034 +#: ../../api.rst:1081 msgid "The user who added the reaction." msgstr "リアクションを追加したユーザー。" -#: ../../api.rst:1039 +#: ../../api.rst:1086 msgid "Called when a message has a reaction removed from it. Similar to on_message_edit, if the message is not found in the internal message cache, then this event will not be called." msgstr "メッセージのリアクションが取り除かれたときに呼び出されます。on_message_editのように、内部のメッセージキャッシュにメッセージがないときには、このイベントは呼び出されません。" -#: ../../api.rst:1045 +#: ../../api.rst:1092 msgid "To get the message being reacted, access it via :attr:`Reaction.message`." msgstr "リアクションの付いたメッセージを取得するには、 :attr:`Reaction.message` を使ってください。" -#: ../../api.rst:1047 +#: ../../api.rst:1094 msgid "This requires both :attr:`Intents.reactions` and :attr:`Intents.members` to be enabled." msgstr "これを使用するには :attr:`Intents.reactions` と :attr:`Intents.members` の両方を有効にしないといけません。" -#: ../../api.rst:1051 +#: ../../api.rst:1098 msgid "Consider using :func:`on_raw_reaction_remove` if you need this and do not want to enable the members intent." msgstr "このイベントが必要でメンバーインテントを有効化したくない場合は :func:`on_raw_reaction_remove` の使用を検討してください。" -#: ../../api.rst:1056 +#: ../../api.rst:1103 +msgid "This event does not have a way of differentiating whether a reaction is a burst reaction (also known as \"super reaction\") or not. If you need this, consider using :func:`on_raw_reaction_remove` instead." +msgstr "" + +#: ../../api.rst:1109 msgid "The user whose reaction was removed." msgstr "リアクションが除去されたユーザー。" -#: ../../api.rst:1061 +#: ../../api.rst:1114 msgid "Called when a message has all its reactions removed from it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_clear` instead." msgstr "メッセージからすべてのリアクションが削除されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_clear` の利用を検討してください。" -#: ../../api.rst:1067 +#: ../../api.rst:1120 msgid "The message that had its reactions cleared." msgstr "リアクションが削除されたメッセージ。" -#: ../../api.rst:1069 +#: ../../api.rst:1122 msgid "The reactions that were removed." msgstr "除去されたリアクション。" -#: ../../api.rst:1074 +#: ../../api.rst:1127 msgid "Called when a message has a specific reaction removed from it. Similar to :func:`on_message_edit`, if the message is not found in the internal message cache, then this event will not be called. Consider using :func:`on_raw_reaction_clear_emoji` instead." msgstr "メッセージから特定の絵文字のリアクションが除去されたときに呼び出されます。 :func:`on_message_edit` と同様に内部メッセージキャッシュにメッセージが見つからない場合は、このイベントは呼び出されません。代わりに :func:`on_raw_reaction_clear_emoji` の利用を検討してください。" -#: ../../api.rst:1082 +#: ../../api.rst:1135 msgid "The reaction that got cleared." msgstr "除去されたリアクション。" -#: ../../api.rst:1088 +#: ../../api.rst:1141 msgid "Called when a message has a reaction added. Unlike :func:`on_reaction_add`, this is called regardless of the state of the internal message cache." msgstr "メッセージにリアクションが追加されたときに呼び出されます。 :func:`on_reaction_add` とは異なり、これは内部のメッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1098 +#: ../../api.rst:1151 msgid "Called when a message has a reaction removed. Unlike :func:`on_reaction_remove`, this is called regardless of the state of the internal message cache." msgstr "メッセージからリアクションが削除されたときに呼び出されます。 :func:`on_reaction_remove` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1108 +#: ../../api.rst:1161 msgid "Called when a message has all its reactions removed. Unlike :func:`on_reaction_clear`, this is called regardless of the state of the internal message cache." msgstr "メッセージからリアクションがすべて削除されたときに呼び出されます。 :func:`on_reaction_clear` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1118 +#: ../../api.rst:1171 msgid "Called when a message has a specific reaction removed from it. Unlike :func:`on_reaction_clear_emoji` this is called regardless of the state of the internal message cache." msgstr "メッセージから特定の絵文字のリアクションがすべて除去されたときに呼び出されます。 :func:`on_reaction_clear_emoji` とは異なり、これは内部メッセージキャッシュの状態に関係なく呼び出されます。" -#: ../../api.rst:1130 +#: ../../api.rst:1183 msgid "Roles" msgstr "Roles" -#: ../../api.rst:1135 +#: ../../api.rst:1188 msgid "Called when a :class:`Guild` creates or deletes a new :class:`Role`." msgstr ":class:`Guild` で :class:`Role` が新しく作成されたか、削除されたときに呼び出されます。" -#: ../../api.rst:1137 +#: ../../api.rst:1190 msgid "To get the guild it belongs to, use :attr:`Role.guild`." msgstr "ギルドを取得するには :attr:`Role.guild` を使用してください。" -#: ../../api.rst:1141 +#: ../../api.rst:1194 msgid "The role that was created or deleted." msgstr "作成、または削除されたロール。" -#: ../../api.rst:1146 +#: ../../api.rst:1199 msgid "Called when a :class:`Role` is changed guild-wide." msgstr ":class:`Role` がギルド全体で変更されたときに呼び出されます。" -#: ../../api.rst:1150 +#: ../../api.rst:1203 msgid "The updated role's old info." msgstr "更新されたロールの更新前情報。" -#: ../../api.rst:1152 +#: ../../api.rst:1205 msgid "The updated role's updated info." msgstr "更新されたロールの更新後情報。" -#: ../../api.rst:1157 +#: ../../api.rst:1210 msgid "Scheduled Events" msgstr "Scheduled Events" -#: ../../api.rst:1162 +#: ../../api.rst:1215 msgid "Called when a :class:`ScheduledEvent` is created or deleted." msgstr ":class:`ScheduledEvent` が作成または削除されたときに呼び出されます。" -#: ../../api.rst:1164 -#: ../../api.rst:1175 -#: ../../api.rst:1197 +#: ../../api.rst:1217 +#: ../../api.rst:1228 +#: ../../api.rst:1250 msgid "This requires :attr:`Intents.guild_scheduled_events` to be enabled." msgstr ":attr:`Intents.guild_scheduled_events` を有効にする必要があります。" -#: ../../api.rst:1168 +#: ../../api.rst:1221 msgid "The scheduled event that was created or deleted." msgstr "作成、または削除されたスケジュールイベント。" -#: ../../api.rst:1173 +#: ../../api.rst:1226 msgid "Called when a :class:`ScheduledEvent` is updated." msgstr ":class:`ScheduledEvent` が変更されたときに呼び出されます。" -#: ../../api.rst:1177 -#: ../../api.rst:1224 -#: ../../api.rst:1376 +#: ../../api.rst:1230 +#: ../../api.rst:1277 +#: ../../api.rst:1429 msgid "The following, but not limited to, examples illustrate when this event is called:" msgstr "これらの場合に限りませんが、例を挙げると、以下の場合に呼び出されます:" -#: ../../api.rst:1179 +#: ../../api.rst:1232 msgid "The scheduled start/end times are changed." msgstr "スケジュールされた開始・終了時刻が変更された。" -#: ../../api.rst:1180 +#: ../../api.rst:1233 msgid "The channel is changed." msgstr "チャンネルが変更された時。" -#: ../../api.rst:1181 +#: ../../api.rst:1234 msgid "The description is changed." msgstr "説明が変更された時。" -#: ../../api.rst:1182 +#: ../../api.rst:1235 msgid "The status is changed." msgstr "ステータスが変更された時。" -#: ../../api.rst:1183 +#: ../../api.rst:1236 msgid "The image is changed." msgstr "画像が変更された時。" -#: ../../api.rst:1187 +#: ../../api.rst:1240 msgid "The scheduled event before the update." msgstr "変更前のスケジュールイベント。" -#: ../../api.rst:1189 +#: ../../api.rst:1242 msgid "The scheduled event after the update." msgstr "変更後のスケジュールイベント。" -#: ../../api.rst:1195 +#: ../../api.rst:1248 msgid "Called when a user is added or removed from a :class:`ScheduledEvent`." msgstr ":class:`ScheduledEvent` からユーザーが追加または削除されたときに呼び出されます。" -#: ../../api.rst:1201 +#: ../../api.rst:1254 msgid "The scheduled event that the user was added or removed from." msgstr "ユーザーが追加または削除されたスケジュールイベント。" -#: ../../api.rst:1203 +#: ../../api.rst:1256 msgid "The user that was added or removed." msgstr "追加・削除されたユーザー。" -#: ../../api.rst:1208 +#: ../../api.rst:1261 msgid "Stages" msgstr "Stages" -#: ../../api.rst:1213 +#: ../../api.rst:1266 msgid "Called when a :class:`StageInstance` is created or deleted for a :class:`StageChannel`." msgstr ":class:`StageChannel` の :class:`StageInstance` が作成または削除されたときに呼び出されます。" -#: ../../api.rst:1217 +#: ../../api.rst:1270 msgid "The stage instance that was created or deleted." msgstr "作成、または削除されたステージインスタンス。" -#: ../../api.rst:1222 +#: ../../api.rst:1275 msgid "Called when a :class:`StageInstance` is updated." msgstr ":class:`StageInstance` が変更されたときに呼び出されます。" -#: ../../api.rst:1226 +#: ../../api.rst:1279 msgid "The topic is changed." msgstr "トピックが変更された時。" -#: ../../api.rst:1227 +#: ../../api.rst:1280 msgid "The privacy level is changed." msgstr "プライバシーレベルが変更された時。" -#: ../../api.rst:1231 +#: ../../api.rst:1284 msgid "The stage instance before the update." msgstr "更新前のステージインスタンス。" -#: ../../api.rst:1233 +#: ../../api.rst:1286 msgid "The stage instance after the update." msgstr "更新後のステージインスタンス。" -#: ../../api.rst:1237 +#: ../../api.rst:1290 msgid "Threads" msgstr "Threads" -#: ../../api.rst:1241 +#: ../../api.rst:1294 msgid "Called whenever a thread is created." msgstr "スレッドが作成されたときに発生します。" -#: ../../api.rst:1243 -#: ../../api.rst:1256 -#: ../../api.rst:1286 -#: ../../api.rst:1310 +#: ../../api.rst:1296 +#: ../../api.rst:1309 +#: ../../api.rst:1339 +#: ../../api.rst:1363 msgid "Note that you can get the guild from :attr:`Thread.guild`." msgstr "ギルドは :attr:`Thread.guild` から取得できます。" -#: ../../api.rst:1249 +#: ../../api.rst:1302 msgid "The thread that was created." msgstr "作成されたスレッド。" -#: ../../api.rst:1254 +#: ../../api.rst:1307 msgid "Called whenever a thread is joined." msgstr "スレッドに参加したときに呼び出されます。" -#: ../../api.rst:1262 +#: ../../api.rst:1315 msgid "The thread that got joined." msgstr "参加したスレッド。" -#: ../../api.rst:1267 +#: ../../api.rst:1320 msgid "Called whenever a thread is updated. If the thread could not be found in the internal cache this event will not be called. Threads will not be in the cache if they are archived." msgstr "スレッドが更新されたときに呼び出されます。スレッドが内部キャッシュに見つからなかった場合、このイベントは呼び出されません。 スレッドがアーカイブされている場合、キャッシュには含まれません。" -#: ../../api.rst:1271 +#: ../../api.rst:1324 msgid "If you need this information use :func:`on_raw_thread_update` instead." msgstr "この情報が必要な場合は、代わりに :func:`on_raw_thread_update` を使用してください。" -#: ../../api.rst:1277 +#: ../../api.rst:1330 msgid "The updated thread's old info." msgstr "古いスレッドの情報。" -#: ../../api.rst:1279 +#: ../../api.rst:1332 msgid "The updated thread's new info." msgstr "新しいスレッドの情報。" -#: ../../api.rst:1284 +#: ../../api.rst:1337 msgid "Called whenever a thread is removed. This is different from a thread being deleted." msgstr "スレッドが除去されたときに呼び出されます。これはスレッドの削除とは異なります。" -#: ../../api.rst:1292 +#: ../../api.rst:1345 msgid "Due to technical limitations, this event might not be called as soon as one expects. Since the library tracks thread membership locally, the API only sends updated thread membership status upon being synced by joining a thread." msgstr "技術的な制約のためこのイベントは期待通りの早さで呼び出されない場合があります。ライブラリーがスレッドの参加をローカルで追跡するため、APIは更新されたスレッドの参加状態をスレッドの参加時にのみ同期します。" -#: ../../api.rst:1299 +#: ../../api.rst:1352 msgid "The thread that got removed." msgstr "削除されたスレッド。" -#: ../../api.rst:1304 +#: ../../api.rst:1357 msgid "Called whenever a thread is deleted. If the thread could not be found in the internal cache this event will not be called. Threads will not be in the cache if they are archived." msgstr "スレッドが削除されたときに呼び出されます。スレッドが内部キャッシュに見つからなかった場合、このイベントは呼び出されません。 スレッドがアーカイブされている場合、キャッシュには含まれません。" -#: ../../api.rst:1308 +#: ../../api.rst:1361 msgid "If you need this information use :func:`on_raw_thread_delete` instead." msgstr "この情報が必要な場合は、代わりに :func:`on_raw_thread_delete` を使用してください。" -#: ../../api.rst:1316 +#: ../../api.rst:1369 msgid "The thread that got deleted." msgstr "削除されたスレッド。" -#: ../../api.rst:1321 +#: ../../api.rst:1374 msgid "Called whenever a thread is updated. Unlike :func:`on_thread_update` this is called regardless of the thread being in the internal thread cache or not." msgstr "スレッドが更新されたときに呼び出されます。 :func:`on_thread_update` とは異なり、更新されたスレッドが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1333 +#: ../../api.rst:1386 msgid "Called whenever a thread is deleted. Unlike :func:`on_thread_delete` this is called regardless of the thread being in the internal thread cache or not." msgstr "スレッドが削除されたときに呼び出されます。 :func:`on_thread_delete` とは異なり、削除されたスレッドが内部キャッシュに存在するか否かにかかわらず呼び出されます。" -#: ../../api.rst:1346 +#: ../../api.rst:1399 msgid "Called when a :class:`ThreadMember` leaves or joins a :class:`Thread`." msgstr ":class:`ThreadMember` が :class:`Thread` に参加したり退出したりしたときに呼び出されます。" -#: ../../api.rst:1348 +#: ../../api.rst:1401 msgid "You can get the thread a member belongs in by accessing :attr:`ThreadMember.thread`." msgstr "メンバーが所属するスレッドは :attr:`ThreadMember.thread` から取得できます。" -#: ../../api.rst:1354 +#: ../../api.rst:1407 msgid "The member who joined or left." msgstr "参加、または脱退したメンバー。" -#: ../../api.rst:1359 +#: ../../api.rst:1412 msgid "Called when a :class:`ThreadMember` leaves a :class:`Thread`. Unlike :func:`on_thread_member_remove` this is called regardless of the member being in the internal thread's members cache or not." msgstr ":class:`ThreadMember` が :class:`Thread` を退出したときに呼び出されます。 :func:`on_thread_member_remove` とは異なり、メンバーが内部スレッドメンバーキャッシュに存在するかどうかに関係なく呼び出されます。" -#: ../../api.rst:1370 +#: ../../api.rst:1423 msgid "Voice" msgstr "Voice" -#: ../../api.rst:1374 +#: ../../api.rst:1427 msgid "Called when a :class:`Member` changes their :class:`VoiceState`." msgstr ":class:`Member` が :class:`VoiceState` を変更したとき呼び出されます。" -#: ../../api.rst:1378 +#: ../../api.rst:1431 msgid "A member joins a voice or stage channel." msgstr "メンバーがボイスチャンネルやステージチャンネルに参加したとき。" -#: ../../api.rst:1379 +#: ../../api.rst:1432 msgid "A member leaves a voice or stage channel." msgstr "メンバーがボイスチャンネルやステージチャンネルから退出したとき。" -#: ../../api.rst:1380 +#: ../../api.rst:1433 msgid "A member is muted or deafened by their own accord." msgstr "メンバーが自身でマイクやスピーカーをミュートしたとき。" -#: ../../api.rst:1381 +#: ../../api.rst:1434 msgid "A member is muted or deafened by a guild administrator." msgstr "メンバーがギルドの管理者によってマイクやスピーカーをミュートされたとき。" -#: ../../api.rst:1383 +#: ../../api.rst:1436 msgid "This requires :attr:`Intents.voice_states` to be enabled." msgstr ":attr:`Intents.voice_states` を有効にする必要があります。" -#: ../../api.rst:1385 +#: ../../api.rst:1438 msgid "The member whose voice states changed." msgstr "ボイスの状態が変わった `Member` 。" -#: ../../api.rst:1387 +#: ../../api.rst:1440 msgid "The voice state prior to the changes." msgstr "更新前のボイス状態。" -#: ../../api.rst:1389 +#: ../../api.rst:1442 msgid "The voice state after the changes." msgstr "更新後のボイス状態。" -#: ../../api.rst:1395 +#: ../../api.rst:1448 msgid "Utility Functions" msgstr "ユーティリティ関数" @@ -4152,15 +4508,15 @@ msgstr "メンションをエスケープするテキスト。" msgid "The text with the mentions removed." msgstr "メンションが削除されたテキスト。" -#: ../../api.rst:1419 +#: ../../api.rst:1472 msgid "A data class which represents a resolved invite returned from :func:`discord.utils.resolve_invite`." msgstr ":func:`discord.utils.resolve_invite` から返された解決済みの招待を表すデータクラス。" -#: ../../api.rst:1423 +#: ../../api.rst:1476 msgid "The invite code." msgstr "招待コード。" -#: ../../api.rst:1429 +#: ../../api.rst:1482 msgid "The id of the scheduled event that the invite refers to." msgstr "招待が参照するスケジュールイベントのID。" @@ -4242,8 +4598,8 @@ msgid "Example Output" msgstr "出力例" #: ../../../discord/utils.py:docstring of discord.utils.format_dt:6 -#: ../../api.rst:3370 -#: ../../api.rst:3390 +#: ../../api.rst:3579 +#: ../../api.rst:3599 msgid "Description" msgstr "説明" @@ -4371,1934 +4727,2049 @@ msgstr "指定されたサイズのチャンクを生成する新しいイテレ msgid "Union[:class:`Iterator`, :class:`AsyncIterator`]" msgstr "Union[:class:`Iterator`, :class:`AsyncIterator`]" -#: ../../api.rst:1448 +#: ../../api.rst:1501 msgid "A type safe sentinel used in the library to represent something as missing. Used to distinguish from ``None`` values." msgstr "ライブラリで見つからないものを表現するために使用されるタイプセーフなセンチネル型。 ``None`` 値と区別するために使用されます。" -#: ../../api.rst:1455 +#: ../../api.rst:1508 msgid "Enumerations" msgstr "列挙型" -#: ../../api.rst:1457 +#: ../../api.rst:1510 msgid "The API provides some enumerations for certain types of strings to avoid the API from being stringly typed in case the strings change in the future." msgstr "APIは、文字列が将来変わることに備え、文字列を直書きするのを防ぐために、いくらかの文字列の列挙型を提供します。" -#: ../../api.rst:1460 +#: ../../api.rst:1513 msgid "All enumerations are subclasses of an internal class which mimics the behaviour of :class:`enum.Enum`." msgstr "列挙型はすべて :class:`enum.Enum` の動作を模倣した内部クラスのサブクラスです。" -#: ../../api.rst:1465 +#: ../../api.rst:1518 msgid "Specifies the type of channel." msgstr "特定チャンネルのチャンネルタイプ。" -#: ../../api.rst:1469 +#: ../../api.rst:1522 msgid "A text channel." msgstr "テキストチャンネル。" -#: ../../api.rst:1472 +#: ../../api.rst:1525 msgid "A voice channel." msgstr "ボイスチャンネル。" -#: ../../api.rst:1475 +#: ../../api.rst:1528 msgid "A private text channel. Also called a direct message." msgstr "プライベートのテキストチャンネル。ダイレクトメッセージとも呼ばれています。" -#: ../../api.rst:1478 +#: ../../api.rst:1531 msgid "A private group text channel." msgstr "プライベートのグループDM。" -#: ../../api.rst:1481 +#: ../../api.rst:1534 msgid "A category channel." msgstr "カテゴリチャンネル。" -#: ../../api.rst:1484 +#: ../../api.rst:1537 msgid "A guild news channel." msgstr "ギルドのニュースチャンネル。" -#: ../../api.rst:1488 +#: ../../api.rst:1541 msgid "A guild stage voice channel." msgstr "ギルドのステージボイスチャンネル。" -#: ../../api.rst:1494 +#: ../../api.rst:1547 msgid "A news thread" msgstr "ニューススレッド。" -#: ../../api.rst:1500 +#: ../../api.rst:1553 msgid "A public thread" msgstr "パブリックスレッド。" -#: ../../api.rst:1506 +#: ../../api.rst:1559 msgid "A private thread" msgstr "プライベートスレッド。" -#: ../../api.rst:1512 +#: ../../api.rst:1565 msgid "A forum channel." msgstr "フォーラムチャンネル。" -#: ../../api.rst:1518 +#: ../../api.rst:1571 +msgid "A media channel." +msgstr "" + +#: ../../api.rst:1577 msgid "Specifies the type of :class:`Message`. This is used to denote if a message is to be interpreted as a system message or a regular message." msgstr ":class:`Message` のタイプを指定します。これは、メッセージが通常のものかシステムメッセージかを判断するのに使用できます。" -#: ../../api.rst:1525 +#: ../../api.rst:1584 #: ../../../discord/message.py:docstring of discord.message.Message:7 msgid "Checks if two messages are equal." msgstr "二つのメッセージが等しいかを比較します。" -#: ../../api.rst:1528 +#: ../../api.rst:1587 #: ../../../discord/message.py:docstring of discord.message.Message:11 msgid "Checks if two messages are not equal." msgstr "二つのメッセージが等しくないかを比較します。" -#: ../../api.rst:1532 +#: ../../api.rst:1591 msgid "The default message type. This is the same as regular messages." msgstr "デフォルトのメッセージ。これは通常のメッセージと同じです。" -#: ../../api.rst:1535 +#: ../../api.rst:1594 msgid "The system message when a user is added to a group private message or a thread." msgstr "ユーザーがグループプライベートメッセージまたはスレッドに追加されたときのシステムメッセージ。" -#: ../../api.rst:1539 +#: ../../api.rst:1598 msgid "The system message when a user is removed from a group private message or a thread." msgstr "ユーザーがグループプライベートメッセージまたはスレッドから削除されたときのシステムメッセージ。" -#: ../../api.rst:1543 +#: ../../api.rst:1602 msgid "The system message denoting call state, e.g. missed call, started call, etc." msgstr "通話の状態を示すシステムメッセージ。例: 不在着信、通話の開始、その他。" -#: ../../api.rst:1547 +#: ../../api.rst:1606 msgid "The system message denoting that a channel's name has been changed." msgstr "チャンネル名の変更を示すシステムメッセージ。" -#: ../../api.rst:1550 +#: ../../api.rst:1609 msgid "The system message denoting that a channel's icon has been changed." msgstr "チャンネルのアイコンの変更を示すシステムメッセージ。" -#: ../../api.rst:1553 +#: ../../api.rst:1612 msgid "The system message denoting that a pinned message has been added to a channel." msgstr "ピン留めの追加を示すシステムメッセージ。" -#: ../../api.rst:1556 +#: ../../api.rst:1615 msgid "The system message denoting that a new member has joined a Guild." msgstr "ギルドの新規メンバーの参加を示すシステムメッセージ。" -#: ../../api.rst:1560 +#: ../../api.rst:1619 msgid "The system message denoting that a member has \"nitro boosted\" a guild." msgstr "メンバーがギルドを「ニトロブースト」したことを表すシステムメッセージ。" -#: ../../api.rst:1563 +#: ../../api.rst:1622 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 1." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル1に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1567 +#: ../../api.rst:1626 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 2." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル2に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1571 +#: ../../api.rst:1630 msgid "The system message denoting that a member has \"nitro boosted\" a guild and it achieved level 3." msgstr "メンバーがギルドを「ニトロブースト」し、それによってギルドがレベル3に到達したことを表すシステムメッセージ。" -#: ../../api.rst:1575 +#: ../../api.rst:1634 msgid "The system message denoting that an announcement channel has been followed." msgstr "アナウンスチャンネルがフォローされたことを表すシステムメッセージ。" -#: ../../api.rst:1580 +#: ../../api.rst:1639 msgid "The system message denoting that a member is streaming in the guild." msgstr "メンバーがギルドでストリーミングしていることを表すシステムメッセージ。" -#: ../../api.rst:1585 +#: ../../api.rst:1644 msgid "The system message denoting that the guild is no longer eligible for Server Discovery." msgstr "ギルドがサーバー発見の資格を持たなくなったことを示すシステムメッセージ。" -#: ../../api.rst:1591 +#: ../../api.rst:1650 msgid "The system message denoting that the guild has become eligible again for Server Discovery." msgstr "ギルドがサーバー発見の資格を再び持ったことを示すシステムメッセージ。" -#: ../../api.rst:1597 +#: ../../api.rst:1656 msgid "The system message denoting that the guild has failed to meet the Server Discovery requirements for one week." msgstr "ギルドが1週間サーバー発見の要件を満たすことに失敗したことを示すシステムメッセージ。" -#: ../../api.rst:1603 +#: ../../api.rst:1662 msgid "The system message denoting that the guild has failed to meet the Server Discovery requirements for 3 weeks in a row." msgstr "ギルドが連続で3週間サーバー発見の要件を満たすことに失敗したというシステムメッセージ。" -#: ../../api.rst:1609 +#: ../../api.rst:1668 msgid "The system message denoting that a thread has been created. This is only sent if the thread has been created from an older message. The period of time required for a message to be considered old cannot be relied upon and is up to Discord." msgstr "スレッドが作成されたことを示すシステムメッセージ。これはスレッドが古いメッセージから作成されたときにのみ送信されます。メッセージが古いものとされる時間の閾値は信頼できずDiscord次第です。" -#: ../../api.rst:1617 +#: ../../api.rst:1676 msgid "The system message denoting that the author is replying to a message." msgstr "送信者がメッセージに返信したことを示すシステムメッセージ。" -#: ../../api.rst:1622 +#: ../../api.rst:1681 msgid "The system message denoting that a slash command was executed." msgstr "スラッシュコマンドを実行したことを示すシステムメッセージ。" -#: ../../api.rst:1627 +#: ../../api.rst:1686 msgid "The system message sent as a reminder to invite people to the guild." msgstr "人々をギルドに招待することのリマインダーとして送信されたシステムメッセージ。" -#: ../../api.rst:1632 +#: ../../api.rst:1691 msgid "The system message denoting the message in the thread that is the one that started the thread's conversation topic." msgstr "スレッドの会話を始めたメッセージであるスレッド内のメッセージを示すシステムメッセージ。" -#: ../../api.rst:1638 +#: ../../api.rst:1697 msgid "The system message denoting that a context menu command was executed." msgstr "コンテキストメニューコマンドを実行したことを示すシステムメッセージ。" -#: ../../api.rst:1643 +#: ../../api.rst:1702 msgid "The system message sent when an AutoMod rule is triggered. This is only sent if the rule is configured to sent an alert when triggered." msgstr "自動管理ルールが発動されたときに送信されるシステムメッセージ。ルールが発動されたときにアラートを送信するように設定されている場合にのみ送信されます。" -#: ../../api.rst:1649 +#: ../../api.rst:1708 msgid "The system message sent when a user purchases or renews a role subscription." msgstr "ユーザーがロールサブスクリプションを購入または更新したときに送信されるシステムメッセージ。" -#: ../../api.rst:1654 +#: ../../api.rst:1713 msgid "The system message sent when a user is given an advertisement to purchase a premium tier for an application during an interaction." msgstr "ユーザーに対し、インタラクション中にアプリケーションのプレミアム版を購入する広告を表示するときに送信されるシステムメッセージ。" -#: ../../api.rst:1660 +#: ../../api.rst:1719 msgid "The system message sent when the stage starts." msgstr "ステージ開始時に送信されるシステムメッセージ。" -#: ../../api.rst:1665 +#: ../../api.rst:1724 msgid "The system message sent when the stage ends." msgstr "ステージ終了時に送信されるシステムメッセージ。" -#: ../../api.rst:1670 +#: ../../api.rst:1729 msgid "The system message sent when the stage speaker changes." msgstr "ステージの発言者が変わるときに送信されるシステムメッセージ。" -#: ../../api.rst:1675 +#: ../../api.rst:1734 msgid "The system message sent when a user is requesting to speak by raising their hands." msgstr "ユーザーが挙手して発言許可を求めているときに送信されるシステムメッセージ。" -#: ../../api.rst:1680 +#: ../../api.rst:1739 msgid "The system message sent when the stage topic changes." msgstr "ステージのトピックが変わるときに送信されるシステムメッセージ。" -#: ../../api.rst:1685 +#: ../../api.rst:1744 msgid "The system message sent when an application's premium subscription is purchased for the guild." msgstr "アプリケーションのプレミアムサブスクリプションがギルドで購入されたときに送信されるシステムメッセージ。" -#: ../../api.rst:1691 +#: ../../api.rst:1750 +msgid "The system message sent when security actions is enabled." +msgstr "" + +#: ../../api.rst:1756 +msgid "The system message sent when security actions is disabled." +msgstr "" + +#: ../../api.rst:1762 +msgid "The system message sent when a raid is reported." +msgstr "" + +#: ../../api.rst:1768 +msgid "The system message sent when a false alarm is reported." +msgstr "" + +#: ../../api.rst:1774 msgid "Represents Discord User flags." msgstr "Discordユーザーフラグを表します。" -#: ../../api.rst:1695 +#: ../../api.rst:1778 msgid "The user is a Discord Employee." msgstr "ユーザーはDiscordの従業員です。" -#: ../../api.rst:1698 +#: ../../api.rst:1781 msgid "The user is a Discord Partner." msgstr "ユーザーはDiscordパートナーです。" -#: ../../api.rst:1701 +#: ../../api.rst:1784 msgid "The user is a HypeSquad Events member." msgstr "ユーザーはHypeSquad Eventsメンバーです。" -#: ../../api.rst:1704 +#: ../../api.rst:1787 msgid "The user is a Bug Hunter." msgstr "ユーザーはバグハンターです。" -#: ../../api.rst:1707 +#: ../../api.rst:1790 msgid "The user has SMS recovery for Multi Factor Authentication enabled." msgstr "ユーザーの多要素認証のSMSリカバリーが有効になっています。" -#: ../../api.rst:1710 +#: ../../api.rst:1793 msgid "The user has dismissed the Discord Nitro promotion." msgstr "ユーザーはDiscord Nitroプロモーションを無視しました。" -#: ../../api.rst:1713 +#: ../../api.rst:1796 msgid "The user is a HypeSquad Bravery member." msgstr "ユーザーはHypeSquad Braveryのメンバーです。" -#: ../../api.rst:1716 +#: ../../api.rst:1799 msgid "The user is a HypeSquad Brilliance member." msgstr "ユーザーはHypeSquad Brillianceのメンバーです。" -#: ../../api.rst:1719 +#: ../../api.rst:1802 msgid "The user is a HypeSquad Balance member." msgstr "ユーザーはHypeSquad Balanceのメンバーです。" -#: ../../api.rst:1722 +#: ../../api.rst:1805 msgid "The user is an Early Supporter." msgstr "ユーザーは早期サポーターです。" -#: ../../api.rst:1725 +#: ../../api.rst:1808 msgid "The user is a Team User." msgstr "ユーザーはチームユーザーです。" -#: ../../api.rst:1728 +#: ../../api.rst:1811 msgid "The user is a system user (i.e. represents Discord officially)." msgstr "ユーザーはシステムユーザーです。(つまり、Discord公式を表しています)" -#: ../../api.rst:1731 +#: ../../api.rst:1814 msgid "The user has an unread system message." msgstr "ユーザーに未読のシステムメッセージがあります。" -#: ../../api.rst:1734 +#: ../../api.rst:1817 msgid "The user is a Bug Hunter Level 2." msgstr "ユーザーはバグハンターレベル2です。" -#: ../../api.rst:1737 +#: ../../api.rst:1820 msgid "The user is a Verified Bot." msgstr "ユーザーは認証済みボットです。" -#: ../../api.rst:1740 +#: ../../api.rst:1823 msgid "The user is an Early Verified Bot Developer." msgstr "ユーザーは早期認証Botデベロッパーです。" -#: ../../api.rst:1743 +#: ../../api.rst:1826 msgid "The user is a Moderator Programs Alumni." msgstr "ユーザーはモデレータープログラム卒業生です。" -#: ../../api.rst:1746 +#: ../../api.rst:1829 msgid "The user is a bot that only uses HTTP interactions and is shown in the online member list." msgstr "ユーザーはHTTPインタラクションのみを使用し、オンラインメンバーのリストに表示されるボットです。" -#: ../../api.rst:1751 +#: ../../api.rst:1834 msgid "The user is flagged as a spammer by Discord." msgstr "ユーザーはDiscordよりスパマーとフラグ付けされました。" -#: ../../api.rst:1757 +#: ../../api.rst:1840 msgid "The user is an active developer." msgstr "ユーザーはアクティブな開発者です。" -#: ../../api.rst:1763 +#: ../../api.rst:1846 msgid "Specifies the type of :class:`Activity`. This is used to check how to interpret the activity itself." msgstr ":class:`Activity` のタイプを指定します。これはアクティビティをどう解釈するか確認するために使われます。" -#: ../../api.rst:1768 +#: ../../api.rst:1851 msgid "An unknown activity type. This should generally not happen." msgstr "不明なアクティビティタイプ。これは通常起こらないはずです。" -#: ../../api.rst:1771 +#: ../../api.rst:1854 msgid "A \"Playing\" activity type." msgstr "プレイ中のアクティビティタイプ。" -#: ../../api.rst:1774 +#: ../../api.rst:1857 msgid "A \"Streaming\" activity type." msgstr "放送中のアクティビティタイプ。" -#: ../../api.rst:1777 +#: ../../api.rst:1860 msgid "A \"Listening\" activity type." msgstr "再生中のアクティビティタイプ。" -#: ../../api.rst:1780 +#: ../../api.rst:1863 msgid "A \"Watching\" activity type." msgstr "視聴中のアクティビティタイプ。" -#: ../../api.rst:1783 +#: ../../api.rst:1866 msgid "A custom activity type." msgstr "カスタムのアクティビティタイプ。" -#: ../../api.rst:1786 +#: ../../api.rst:1869 msgid "A competing activity type." msgstr "競争中のアクティビティタイプ。" -#: ../../api.rst:1792 +#: ../../api.rst:1875 msgid "Specifies a :class:`Guild`\\'s verification level, which is the criteria in which a member must meet before being able to send messages to the guild." msgstr ":class:`Guild` の認証レベルを指定します。これは、メンバーがギルドにメッセージを送信できるようになるまでの条件です。" -#: ../../api.rst:1801 +#: ../../api.rst:1884 msgid "Checks if two verification levels are equal." msgstr "認証レベルが等しいか確認します。" -#: ../../api.rst:1804 +#: ../../api.rst:1887 msgid "Checks if two verification levels are not equal." msgstr "認証レベルが等しくないか確認します。" -#: ../../api.rst:1807 +#: ../../api.rst:1890 msgid "Checks if a verification level is higher than another." msgstr "認証レベルがあるレベルより厳しいか確認します。" -#: ../../api.rst:1810 +#: ../../api.rst:1893 msgid "Checks if a verification level is lower than another." msgstr "認証レベルがあるレベルより緩いか確認します。" -#: ../../api.rst:1813 +#: ../../api.rst:1896 msgid "Checks if a verification level is higher or equal to another." msgstr "認証レベルがあるレベルと同じ、又は厳しいか確認します。" -#: ../../api.rst:1816 +#: ../../api.rst:1899 msgid "Checks if a verification level is lower or equal to another." msgstr "認証レベルがあるレベルと同じ、又は緩いか確認します。" -#: ../../api.rst:1820 +#: ../../api.rst:1903 msgid "No criteria set." msgstr "無制限。" -#: ../../api.rst:1823 +#: ../../api.rst:1906 msgid "Member must have a verified email on their Discord account." msgstr "メンバーはDiscordアカウントのメール認証を済ませないといけません。" -#: ../../api.rst:1826 +#: ../../api.rst:1909 msgid "Member must have a verified email and be registered on Discord for more than five minutes." msgstr "メンバーはメール認証をし、かつアカウント登録から5分経過しないといけません。" -#: ../../api.rst:1830 +#: ../../api.rst:1913 msgid "Member must have a verified email, be registered on Discord for more than five minutes, and be a member of the guild itself for more than ten minutes." msgstr "メンバーはメール認証をし、Discordのアカウント登録から5分経過し、かつ10分以上ギルドに所属していないといけません。" -#: ../../api.rst:1835 +#: ../../api.rst:1918 msgid "Member must have a verified phone on their Discord account." msgstr "メンバーはDiscordアカウントの電話番号認証を済ませないといけません。" -#: ../../api.rst:1839 +#: ../../api.rst:1922 msgid "Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default." msgstr ":class:`Guild` の通知対象のデフォルト設定をすべてのメッセージ、またはメンションのみに指定します。" -#: ../../api.rst:1847 +#: ../../api.rst:1930 msgid "Checks if two notification levels are equal." msgstr "通知レベルが等しいか確認します。" -#: ../../api.rst:1850 +#: ../../api.rst:1933 msgid "Checks if two notification levels are not equal." msgstr "通知レベルが等しくないか確認します。" -#: ../../api.rst:1853 +#: ../../api.rst:1936 msgid "Checks if a notification level is higher than another." msgstr "通知レベルがあるレベルより高いか確認します。" -#: ../../api.rst:1856 +#: ../../api.rst:1939 msgid "Checks if a notification level is lower than another." msgstr "通知レベルがあるレベルより低いか確認します。" -#: ../../api.rst:1859 +#: ../../api.rst:1942 msgid "Checks if a notification level is higher or equal to another." msgstr "通知レベルがあるレベルと同じ、又は高いか確認します。" -#: ../../api.rst:1862 +#: ../../api.rst:1945 msgid "Checks if a notification level is lower or equal to another." msgstr "通知レベルがあるレベルと同じ、又は低いか確認します。" -#: ../../api.rst:1866 +#: ../../api.rst:1949 msgid "Members receive notifications for every message regardless of them being mentioned." msgstr "メンバーは、メンションされているかどうかに関わらず、すべてのメッセージの通知を受け取ります。" -#: ../../api.rst:1869 +#: ../../api.rst:1952 msgid "Members receive notifications for messages they are mentioned in." msgstr "メンバーは自分がメンションされているメッセージの通知のみ受け取ります。" -#: ../../api.rst:1873 +#: ../../api.rst:1956 msgid "Specifies a :class:`Guild`\\'s explicit content filter, which is the machine learning algorithms that Discord uses to detect if an image contains pornography or otherwise explicit content." msgstr ":class:`Guild` の不適切な表現のフィルターを指定します。これはDiscordがポルノ画像や不適切な表現を検出するために使用している機械学習アルゴリズムです。" -#: ../../api.rst:1883 +#: ../../api.rst:1966 msgid "Checks if two content filter levels are equal." msgstr "表現のフィルターのレベルが等しいか確認します。" -#: ../../api.rst:1886 +#: ../../api.rst:1969 msgid "Checks if two content filter levels are not equal." msgstr "表現のフィルターのレベルが等しくないか確認します。" -#: ../../api.rst:1889 +#: ../../api.rst:1972 msgid "Checks if a content filter level is higher than another." msgstr "表現のフィルターのレベルが他のレベルより大きいか確認します。" -#: ../../api.rst:1892 +#: ../../api.rst:1975 msgid "Checks if a content filter level is lower than another." msgstr "表現のフィルターのレベルが他のレベルより小さいか確認します。" -#: ../../api.rst:1895 +#: ../../api.rst:1978 msgid "Checks if a content filter level is higher or equal to another." msgstr "表現のフィルターのレベルが他のレベルより大きい、または等しいか確認します。" -#: ../../api.rst:1898 +#: ../../api.rst:1981 msgid "Checks if a content filter level is lower or equal to another." msgstr "表現のフィルターのレベルが他のレベルより小さい、または等しいか確認します。" -#: ../../api.rst:1902 +#: ../../api.rst:1985 msgid "The guild does not have the content filter enabled." msgstr "ギルドで表現のフィルターが有効ではない。" -#: ../../api.rst:1905 +#: ../../api.rst:1988 msgid "The guild has the content filter enabled for members without a role." msgstr "ギルドでロールを持たないメンバーに対して表現のフィルターが有効化されている。" -#: ../../api.rst:1908 +#: ../../api.rst:1991 msgid "The guild has the content filter enabled for every member." msgstr "ギルドで、すべてのメンバーに対して表現のフィルターが有効化されている。" -#: ../../api.rst:1912 +#: ../../api.rst:1995 msgid "Specifies a :class:`Member` 's status." msgstr ":class:`Member` のステータスを指定します。" -#: ../../api.rst:1916 +#: ../../api.rst:1999 msgid "The member is online." msgstr "メンバーがオンライン。" -#: ../../api.rst:1919 +#: ../../api.rst:2002 msgid "The member is offline." msgstr "メンバーがオフライン。" -#: ../../api.rst:1922 +#: ../../api.rst:2005 msgid "The member is idle." msgstr "メンバーが退席中。" -#: ../../api.rst:1925 +#: ../../api.rst:2008 msgid "The member is \"Do Not Disturb\"." msgstr "メンバーが取り込み中。" -#: ../../api.rst:1928 +#: ../../api.rst:2011 msgid "An alias for :attr:`dnd`." msgstr ":attr:`dnd` のエイリアス。" -#: ../../api.rst:1931 +#: ../../api.rst:2014 msgid "The member is \"invisible\". In reality, this is only used when sending a presence a la :meth:`Client.change_presence`. When you receive a user's presence this will be :attr:`offline` instead." msgstr "メンバーがオンライン状態を隠す。実際には、これは :meth:`Client.change_presence` でプレゼンスを送信する時のみ使用します。ユーザーのプレゼンスを受け取った場合、これは :attr:`offline` に置き換えられます。" -#: ../../api.rst:1938 +#: ../../api.rst:2021 msgid "Represents the type of action being done for a :class:`AuditLogEntry`\\, which is retrievable via :meth:`Guild.audit_logs`." msgstr ":class:`AuditLogEntry` で行われた動作の種類を取得します。AuditLogEntryは :meth:`Guild.audit_logs` で取得可能です。" -#: ../../api.rst:1943 +#: ../../api.rst:2026 msgid "The guild has updated. Things that trigger this include:" msgstr "ギルドの更新。このトリガーとなるものは以下のとおりです。" -#: ../../api.rst:1945 +#: ../../api.rst:2028 msgid "Changing the guild vanity URL" msgstr "ギルドのvanity URLの変更" -#: ../../api.rst:1946 +#: ../../api.rst:2029 msgid "Changing the guild invite splash" msgstr "ギルドの招待時のスプラッシュ画像の変更" -#: ../../api.rst:1947 +#: ../../api.rst:2030 msgid "Changing the guild AFK channel or timeout" msgstr "ギルドのAFKチャンネル、またはタイムアウトの変更" -#: ../../api.rst:1948 +#: ../../api.rst:2031 msgid "Changing the guild voice server region" msgstr "ギルドの音声通話のサーバーリージョンの変更" -#: ../../api.rst:1949 +#: ../../api.rst:2032 msgid "Changing the guild icon, banner, or discovery splash" msgstr "ギルドのアイコン、バナー、ディスカバリースプラッシュの変更" -#: ../../api.rst:1950 +#: ../../api.rst:2033 msgid "Changing the guild moderation settings" msgstr "ギルドの管理設定の変更" -#: ../../api.rst:1951 +#: ../../api.rst:2034 msgid "Changing things related to the guild widget" msgstr "ギルドのウィジェットに関連するものの変更" -#: ../../api.rst:1953 +#: ../../api.rst:2036 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Guild`." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は :class:`Guild` になります。" -#: ../../api.rst:1956 -#: ../../api.rst:1992 -#: ../../api.rst:2011 -#: ../../api.rst:2036 -#: ../../api.rst:2058 +#: ../../api.rst:2039 +#: ../../api.rst:2075 +#: ../../api.rst:2094 +#: ../../api.rst:2119 +#: ../../api.rst:2141 msgid "Possible attributes for :class:`AuditLogDiff`:" msgstr ":class:`AuditLogDiff` から、以下の属性を参照できます:" -#: ../../api.rst:1958 +#: ../../api.rst:2041 msgid ":attr:`~AuditLogDiff.afk_channel`" msgstr ":attr:`~AuditLogDiff.afk_channel`" -#: ../../api.rst:1959 +#: ../../api.rst:2042 msgid ":attr:`~AuditLogDiff.system_channel`" msgstr ":attr:`~AuditLogDiff.system_channel`" -#: ../../api.rst:1960 +#: ../../api.rst:2043 msgid ":attr:`~AuditLogDiff.afk_timeout`" msgstr ":attr:`~AuditLogDiff.afk_timeout`" -#: ../../api.rst:1961 +#: ../../api.rst:2044 msgid ":attr:`~AuditLogDiff.default_notifications`" msgstr ":attr:`~AuditLogDiff.default_notifications`" -#: ../../api.rst:1962 +#: ../../api.rst:2045 msgid ":attr:`~AuditLogDiff.explicit_content_filter`" msgstr ":attr:`~AuditLogDiff.explicit_content_filter`" -#: ../../api.rst:1963 +#: ../../api.rst:2046 msgid ":attr:`~AuditLogDiff.mfa_level`" msgstr ":attr:`~AuditLogDiff.mfa_level`" -#: ../../api.rst:1964 -#: ../../api.rst:1994 -#: ../../api.rst:2013 -#: ../../api.rst:2038 -#: ../../api.rst:2215 +#: ../../api.rst:2047 +#: ../../api.rst:2077 +#: ../../api.rst:2096 +#: ../../api.rst:2121 +#: ../../api.rst:2308 msgid ":attr:`~AuditLogDiff.name`" msgstr ":attr:`~AuditLogDiff.name`" -#: ../../api.rst:1965 +#: ../../api.rst:2048 msgid ":attr:`~AuditLogDiff.owner`" msgstr ":attr:`~AuditLogDiff.owner`" -#: ../../api.rst:1966 +#: ../../api.rst:2049 msgid ":attr:`~AuditLogDiff.splash`" msgstr ":attr:`~AuditLogDiff.splash`" -#: ../../api.rst:1967 +#: ../../api.rst:2050 msgid ":attr:`~AuditLogDiff.discovery_splash`" msgstr ":attr:`~AuditLogDiff.discovery_splash`" -#: ../../api.rst:1968 -#: ../../api.rst:2213 -#: ../../api.rst:2236 +#: ../../api.rst:2051 +#: ../../api.rst:2306 +#: ../../api.rst:2329 msgid ":attr:`~AuditLogDiff.icon`" msgstr ":attr:`~AuditLogDiff.icon`" -#: ../../api.rst:1969 +#: ../../api.rst:2052 msgid ":attr:`~AuditLogDiff.banner`" msgstr ":attr:`~AuditLogDiff.banner`" -#: ../../api.rst:1970 +#: ../../api.rst:2053 msgid ":attr:`~AuditLogDiff.vanity_url_code`" msgstr ":attr:`~AuditLogDiff.vanity_url_code`" -#: ../../api.rst:1971 -#: ../../api.rst:2510 -#: ../../api.rst:2529 -#: ../../api.rst:2548 +#: ../../api.rst:2054 +#: ../../api.rst:2603 +#: ../../api.rst:2622 +#: ../../api.rst:2641 msgid ":attr:`~AuditLogDiff.description`" msgstr ":attr:`~AuditLogDiff.description`" -#: ../../api.rst:1972 +#: ../../api.rst:2055 msgid ":attr:`~AuditLogDiff.preferred_locale`" msgstr ":attr:`~AuditLogDiff.preferred_locale`" -#: ../../api.rst:1973 +#: ../../api.rst:2056 msgid ":attr:`~AuditLogDiff.prune_delete_days`" msgstr ":attr:`~AuditLogDiff.prune_delete_days`" -#: ../../api.rst:1974 +#: ../../api.rst:2057 msgid ":attr:`~AuditLogDiff.public_updates_channel`" msgstr ":attr:`~AuditLogDiff.public_updates_channel`" -#: ../../api.rst:1975 +#: ../../api.rst:2058 msgid ":attr:`~AuditLogDiff.rules_channel`" msgstr ":attr:`~AuditLogDiff.rules_channel`" -#: ../../api.rst:1976 +#: ../../api.rst:2059 msgid ":attr:`~AuditLogDiff.verification_level`" msgstr ":attr:`~AuditLogDiff.verification_level`" -#: ../../api.rst:1977 +#: ../../api.rst:2060 msgid ":attr:`~AuditLogDiff.widget_channel`" msgstr ":attr:`~AuditLogDiff.widget_channel`" -#: ../../api.rst:1978 +#: ../../api.rst:2061 msgid ":attr:`~AuditLogDiff.widget_enabled`" msgstr ":attr:`~AuditLogDiff.widget_enabled`" -#: ../../api.rst:1979 +#: ../../api.rst:2062 msgid ":attr:`~AuditLogDiff.premium_progress_bar_enabled`" msgstr ":attr:`~AuditLogDiff.premium_progress_bar_enabled`" -#: ../../api.rst:1980 +#: ../../api.rst:2063 msgid ":attr:`~AuditLogDiff.system_channel_flags`" msgstr ":attr:`~AuditLogDiff.system_channel_flags`" -#: ../../api.rst:1984 +#: ../../api.rst:2067 msgid "A new channel was created." msgstr "チャンネルの作成。" -#: ../../api.rst:1986 +#: ../../api.rst:2069 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is either a :class:`abc.GuildChannel` or :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`abc.GuildChannel` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:1989 +#: ../../api.rst:2072 msgid "A more filled out object in the :class:`Object` case can be found by using :attr:`~AuditLogEntry.after`." msgstr ":class:`Object` の場合、 :attr:`~AuditLogEntry.after` を使用して、より詳細な情報を持つオブジェクトを見つけることができます。" -#: ../../api.rst:1995 -#: ../../api.rst:2014 -#: ../../api.rst:2039 -#: ../../api.rst:2063 -#: ../../api.rst:2079 +#: ../../api.rst:2078 +#: ../../api.rst:2097 +#: ../../api.rst:2122 +#: ../../api.rst:2146 +#: ../../api.rst:2162 msgid ":attr:`~AuditLogDiff.type`" msgstr ":attr:`~AuditLogDiff.type`" -#: ../../api.rst:1996 -#: ../../api.rst:2016 -#: ../../api.rst:2040 +#: ../../api.rst:2079 +#: ../../api.rst:2099 +#: ../../api.rst:2123 msgid ":attr:`~AuditLogDiff.overwrites`" msgstr ":attr:`~AuditLogDiff.overwrites`" -#: ../../api.rst:2000 +#: ../../api.rst:2083 msgid "A channel was updated. Things that trigger this include:" msgstr "チャンネルの更新。これのトリガーとなるものは以下の通りです。" -#: ../../api.rst:2002 +#: ../../api.rst:2085 msgid "The channel name or topic was changed" msgstr "チャンネルのチャンネルトピックの変更、または名前の変更。" -#: ../../api.rst:2003 +#: ../../api.rst:2086 msgid "The channel bitrate was changed" msgstr "チャンネルのビットレートの変更。" -#: ../../api.rst:2005 -#: ../../api.rst:2049 +#: ../../api.rst:2088 +#: ../../api.rst:2132 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`abc.GuildChannel` or :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`abc.GuildChannel` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:2008 +#: ../../api.rst:2091 msgid "A more filled out object in the :class:`Object` case can be found by using :attr:`~AuditLogEntry.after` or :attr:`~AuditLogEntry.before`." msgstr ":class:`Object` の場合、 :attr:`~AuditLogEntry.after` または :attr:`~AuditLogEntry.before` を使用して、より詳細な情報を持つオブジェクトを見つけることができます。" -#: ../../api.rst:2015 +#: ../../api.rst:2098 msgid ":attr:`~AuditLogDiff.position`" msgstr ":attr:`~AuditLogDiff.position`" -#: ../../api.rst:2017 -#: ../../api.rst:2470 -#: ../../api.rst:2485 +#: ../../api.rst:2100 +#: ../../api.rst:2563 +#: ../../api.rst:2578 msgid ":attr:`~AuditLogDiff.topic`" msgstr ":attr:`~AuditLogDiff.topic`" -#: ../../api.rst:2018 +#: ../../api.rst:2101 msgid ":attr:`~AuditLogDiff.bitrate`" msgstr ":attr:`~AuditLogDiff.bitrate`" -#: ../../api.rst:2019 +#: ../../api.rst:2102 msgid ":attr:`~AuditLogDiff.rtc_region`" msgstr ":attr:`~AuditLogDiff.rtc_region`" -#: ../../api.rst:2020 +#: ../../api.rst:2103 msgid ":attr:`~AuditLogDiff.video_quality_mode`" msgstr ":attr:`~AuditLogDiff.video_quality_mode`" -#: ../../api.rst:2021 +#: ../../api.rst:2104 msgid ":attr:`~AuditLogDiff.default_auto_archive_duration`" msgstr ":attr:`~AuditLogDiff.default_auto_archive_duration`" -#: ../../api.rst:2022 -#: ../../api.rst:2042 +#: ../../api.rst:2105 +#: ../../api.rst:2125 msgid ":attr:`~AuditLogDiff.nsfw`" msgstr ":attr:`~AuditLogDiff.nsfw`" -#: ../../api.rst:2023 -#: ../../api.rst:2043 +#: ../../api.rst:2106 +#: ../../api.rst:2126 msgid ":attr:`~AuditLogDiff.slowmode_delay`" msgstr ":attr:`~AuditLogDiff.slowmode_delay`" -#: ../../api.rst:2024 +#: ../../api.rst:2107 msgid ":attr:`~AuditLogDiff.user_limit`" msgstr ":attr:`~AuditLogDiff.user_limit`" -#: ../../api.rst:2028 +#: ../../api.rst:2111 msgid "A channel was deleted." msgstr "チャンネルの削除。" -#: ../../api.rst:2030 +#: ../../api.rst:2113 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is an :class:`Object` with an ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2033 +#: ../../api.rst:2116 msgid "A more filled out object can be found by using the :attr:`~AuditLogEntry.before` object." msgstr ":attr:`~AuditLogEntry.before` オブジェクトを使用すると、より詳細な情報が見つかります。" -#: ../../api.rst:2041 +#: ../../api.rst:2124 msgid ":attr:`~AuditLogDiff.flags`" msgstr ":attr:`~AuditLogDiff.flags`" -#: ../../api.rst:2047 +#: ../../api.rst:2130 msgid "A channel permission overwrite was created." msgstr "チャンネルにおける権限の上書き設定の作成。" -#: ../../api.rst:2052 +#: ../../api.rst:2135 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is either a :class:`Role` or :class:`Member`. If the object is not found then it is a :class:`Object` with an ID being filled, a name, and a ``type`` attribute set to either ``'role'`` or ``'member'`` to help dictate what type of ID it is." msgstr "この場合には、 :attr:`~AuditLogEntry.extra` は :class:`Role` か :class:`Member` です。もしオブジェクトが見つからない場合はid、name、 ``'role'`` か ``'member'`` である ``type`` 属性がある :class:`Object` です。" -#: ../../api.rst:2060 -#: ../../api.rst:2076 -#: ../../api.rst:2091 +#: ../../api.rst:2143 +#: ../../api.rst:2159 +#: ../../api.rst:2174 msgid ":attr:`~AuditLogDiff.deny`" msgstr ":attr:`~AuditLogDiff.deny`" -#: ../../api.rst:2061 -#: ../../api.rst:2077 -#: ../../api.rst:2092 +#: ../../api.rst:2144 +#: ../../api.rst:2160 +#: ../../api.rst:2175 msgid ":attr:`~AuditLogDiff.allow`" msgstr ":attr:`~AuditLogDiff.allow`" -#: ../../api.rst:2062 -#: ../../api.rst:2078 -#: ../../api.rst:2093 +#: ../../api.rst:2145 +#: ../../api.rst:2161 +#: ../../api.rst:2176 msgid ":attr:`~AuditLogDiff.id`" msgstr ":attr:`~AuditLogDiff.id`" -#: ../../api.rst:2067 +#: ../../api.rst:2150 msgid "A channel permission overwrite was changed, this is typically when the permission values change." msgstr "チャンネルの権限の上書きの変更。典型的な例は、権限が変更された場合です。" -#: ../../api.rst:2070 -#: ../../api.rst:2085 +#: ../../api.rst:2153 +#: ../../api.rst:2168 msgid "See :attr:`overwrite_create` for more information on how the :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields are set." msgstr ":attr:`overwrite_create` に、 :attr:`~AuditLogEntry.target` と :attr:`~AuditLogEntry.extra` についての説明があります。" -#: ../../api.rst:2083 +#: ../../api.rst:2166 msgid "A channel permission overwrite was deleted." msgstr "チャンネルにおける権限の上書き設定の削除。" -#: ../../api.rst:2098 +#: ../../api.rst:2181 msgid "A member was kicked." msgstr "メンバーのキック。" -#: ../../api.rst:2100 +#: ../../api.rst:2183 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got kicked." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、キックされた :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2103 -#: ../../api.rst:2118 -#: ../../api.rst:2127 -#: ../../api.rst:2136 +#: ../../api.rst:2186 +#: ../../api.rst:2251 +#: ../../api.rst:2278 +#: ../../api.rst:2486 +msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with one attribute:" +msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" + +#: ../../api.rst:2189 +#: ../../api.rst:2254 +msgid "``integration_type``: An optional string that denotes the type of integration that did the action." +msgstr "" + +#: ../../api.rst:2191 +#: ../../api.rst:2206 +#: ../../api.rst:2215 +#: ../../api.rst:2224 msgid "When this is the action, :attr:`~AuditLogEntry.changes` is empty." msgstr "これが上記のactionなら、:attr:`~AuditLogEntry.changes` は空になります。" -#: ../../api.rst:2107 +#: ../../api.rst:2195 msgid "A member prune was triggered." msgstr "非アクティブメンバーの一括キック。" -#: ../../api.rst:2109 +#: ../../api.rst:2197 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is set to ``None``." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は ``None`` に設定されます。" -#: ../../api.rst:2112 -#: ../../api.rst:2172 -#: ../../api.rst:2380 -#: ../../api.rst:2407 -#: ../../api.rst:2422 +#: ../../api.rst:2200 +#: ../../api.rst:2265 +#: ../../api.rst:2473 +#: ../../api.rst:2500 +#: ../../api.rst:2515 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with two attributes:" msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" -#: ../../api.rst:2115 +#: ../../api.rst:2203 msgid "``delete_member_days``: An integer specifying how far the prune was." msgstr "``delete_members_days`` : 一括キック対象の期間を示す整数。" -#: ../../api.rst:2116 +#: ../../api.rst:2204 msgid "``members_removed``: An integer specifying how many members were removed." msgstr "``members_removed`` : 除去されたメンバーの数を示す整数。" -#: ../../api.rst:2122 +#: ../../api.rst:2210 msgid "A member was banned." msgstr "メンバーのBAN。" -#: ../../api.rst:2124 +#: ../../api.rst:2212 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got banned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、BANされた :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2131 +#: ../../api.rst:2219 msgid "A member was unbanned." msgstr "メンバーのBANの解除。" -#: ../../api.rst:2133 +#: ../../api.rst:2221 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`User` or :class:`Object` who got unbanned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、BAN解除された :class:`User` あるいは :class:`Object` になります。" -#: ../../api.rst:2140 +#: ../../api.rst:2228 msgid "A member has updated. This triggers in the following situations:" msgstr "メンバーの何らかの更新。これのトリガーとなるのは以下の場合です:" -#: ../../api.rst:2142 +#: ../../api.rst:2230 msgid "A nickname was changed" msgstr "メンバーのニックネームの変更。" -#: ../../api.rst:2143 +#: ../../api.rst:2231 msgid "They were server muted or deafened (or it was undo'd)" msgstr "サーバー側でミュートやスピーカーミュートされた(あるいは解除された)場合。" -#: ../../api.rst:2145 +#: ../../api.rst:2233 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who got updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、更新の行われた :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2150 +#: ../../api.rst:2238 msgid ":attr:`~AuditLogDiff.nick`" msgstr ":attr:`~AuditLogDiff.nick`" -#: ../../api.rst:2151 +#: ../../api.rst:2239 msgid ":attr:`~AuditLogDiff.mute`" msgstr ":attr:`~AuditLogDiff.mute`" -#: ../../api.rst:2152 +#: ../../api.rst:2240 msgid ":attr:`~AuditLogDiff.deaf`" msgstr ":attr:`~AuditLogDiff.deaf`" -#: ../../api.rst:2153 +#: ../../api.rst:2241 msgid ":attr:`~AuditLogDiff.timed_out_until`" msgstr ":attr:`~AuditLogDiff.timed_out_until`" -#: ../../api.rst:2157 +#: ../../api.rst:2245 msgid "A member's role has been updated. This triggers when a member either gains a role or loses a role." msgstr "メンバーのロールの更新。これは、メンバーがロールを得たり、失った場合に発生します。" -#: ../../api.rst:2160 +#: ../../api.rst:2248 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who got the role." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ロールの更新が行われた :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2165 +#: ../../api.rst:2258 msgid ":attr:`~AuditLogDiff.roles`" msgstr ":attr:`~AuditLogDiff.roles`" -#: ../../api.rst:2169 +#: ../../api.rst:2262 msgid "A member's voice channel has been updated. This triggers when a member is moved to a different voice channel." msgstr "メンバーのボイスチャンネルの更新。これは、メンバーが他のボイスチャンネルに移動させられた時に発生します。" -#: ../../api.rst:2175 +#: ../../api.rst:2268 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the members were moved." msgstr "``channel`` : メンバーの移動先の :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2176 +#: ../../api.rst:2269 msgid "``count``: An integer specifying how many members were moved." msgstr "``count`` : 移動されたメンバーの数を示す整数。" -#: ../../api.rst:2182 +#: ../../api.rst:2275 msgid "A member's voice state has changed. This triggers when a member is force disconnected from voice." msgstr "メンバーのボイス状態の変更。これはメンバーがボイスから強制的に切断された場合に発生します。" -#: ../../api.rst:2185 -#: ../../api.rst:2393 -msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with one attribute:" -msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の属性を持つプロキシオブジェクトになります:" - -#: ../../api.rst:2188 +#: ../../api.rst:2281 msgid "``count``: An integer specifying how many members were disconnected." msgstr "``count`` : 切断されたメンバーの数を示す整数。" -#: ../../api.rst:2194 +#: ../../api.rst:2287 msgid "A bot was added to the guild." msgstr "ボットのギルドへの追加。" -#: ../../api.rst:2196 +#: ../../api.rst:2289 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` which was added to the guild." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ギルドに追加された :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2203 +#: ../../api.rst:2296 msgid "A new role was created." msgstr "新しいロールの作成。" -#: ../../api.rst:2205 -#: ../../api.rst:2228 -#: ../../api.rst:2245 +#: ../../api.rst:2298 +#: ../../api.rst:2321 +#: ../../api.rst:2338 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Role` or a :class:`Object` with the ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、IDが設定されている :class:`Role` か、 :class:`Object` のいずれかになります。" -#: ../../api.rst:2210 -#: ../../api.rst:2233 -#: ../../api.rst:2250 +#: ../../api.rst:2303 +#: ../../api.rst:2326 +#: ../../api.rst:2343 msgid ":attr:`~AuditLogDiff.colour`" msgstr ":attr:`~AuditLogDiff.colour`" -#: ../../api.rst:2211 -#: ../../api.rst:2234 -#: ../../api.rst:2251 +#: ../../api.rst:2304 +#: ../../api.rst:2327 +#: ../../api.rst:2344 msgid ":attr:`~AuditLogDiff.mentionable`" msgstr ":attr:`~AuditLogDiff.mentionable`" -#: ../../api.rst:2212 -#: ../../api.rst:2235 -#: ../../api.rst:2252 +#: ../../api.rst:2305 +#: ../../api.rst:2328 +#: ../../api.rst:2345 msgid ":attr:`~AuditLogDiff.hoist`" msgstr ":attr:`~AuditLogDiff.hoist`" -#: ../../api.rst:2214 -#: ../../api.rst:2237 +#: ../../api.rst:2307 +#: ../../api.rst:2330 msgid ":attr:`~AuditLogDiff.unicode_emoji`" msgstr ":attr:`~AuditLogDiff.unicode_emoji`" -#: ../../api.rst:2216 -#: ../../api.rst:2239 -#: ../../api.rst:2254 +#: ../../api.rst:2309 +#: ../../api.rst:2332 +#: ../../api.rst:2347 msgid ":attr:`~AuditLogDiff.permissions`" msgstr ":attr:`~AuditLogDiff.permissions`" -#: ../../api.rst:2220 +#: ../../api.rst:2313 msgid "A role was updated. This triggers in the following situations:" msgstr "ロールの何らかの更新。これのトリガーとなるのは以下の場合です:" -#: ../../api.rst:2222 +#: ../../api.rst:2315 msgid "The name has changed" msgstr "名前の更新。" -#: ../../api.rst:2223 +#: ../../api.rst:2316 msgid "The permissions have changed" msgstr "権限の更新。" -#: ../../api.rst:2224 +#: ../../api.rst:2317 msgid "The colour has changed" msgstr "色の更新。" -#: ../../api.rst:2225 +#: ../../api.rst:2318 msgid "The role icon (or unicode emoji) has changed" msgstr "ロールアイコン (または Unicode 絵文字)の変更。" -#: ../../api.rst:2226 +#: ../../api.rst:2319 msgid "Its hoist/mentionable state has changed" msgstr "ロールメンバーのオンライン表示、ロールへのメンションの許可の変更。" -#: ../../api.rst:2243 +#: ../../api.rst:2336 msgid "A role was deleted." msgstr "ロールの削除。" -#: ../../api.rst:2258 +#: ../../api.rst:2351 msgid "An invite was created." msgstr "招待の作成。" -#: ../../api.rst:2260 +#: ../../api.rst:2353 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was created." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は作成された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2265 -#: ../../api.rst:2289 +#: ../../api.rst:2358 +#: ../../api.rst:2382 msgid ":attr:`~AuditLogDiff.max_age`" msgstr ":attr:`~AuditLogDiff.max_age`" -#: ../../api.rst:2266 -#: ../../api.rst:2290 +#: ../../api.rst:2359 +#: ../../api.rst:2383 msgid ":attr:`~AuditLogDiff.code`" msgstr ":attr:`~AuditLogDiff.code`" -#: ../../api.rst:2267 -#: ../../api.rst:2291 +#: ../../api.rst:2360 +#: ../../api.rst:2384 msgid ":attr:`~AuditLogDiff.temporary`" msgstr ":attr:`~AuditLogDiff.temporary`" -#: ../../api.rst:2268 -#: ../../api.rst:2292 +#: ../../api.rst:2361 +#: ../../api.rst:2385 msgid ":attr:`~AuditLogDiff.inviter`" msgstr ":attr:`~AuditLogDiff.inviter`" -#: ../../api.rst:2269 -#: ../../api.rst:2293 -#: ../../api.rst:2306 -#: ../../api.rst:2322 -#: ../../api.rst:2335 +#: ../../api.rst:2362 +#: ../../api.rst:2386 +#: ../../api.rst:2399 +#: ../../api.rst:2415 +#: ../../api.rst:2428 msgid ":attr:`~AuditLogDiff.channel`" msgstr ":attr:`~AuditLogDiff.channel`" -#: ../../api.rst:2270 -#: ../../api.rst:2294 +#: ../../api.rst:2363 +#: ../../api.rst:2387 msgid ":attr:`~AuditLogDiff.uses`" msgstr ":attr:`~AuditLogDiff.uses`" -#: ../../api.rst:2271 -#: ../../api.rst:2295 +#: ../../api.rst:2364 +#: ../../api.rst:2388 msgid ":attr:`~AuditLogDiff.max_uses`" msgstr ":attr:`~AuditLogDiff.max_uses`" -#: ../../api.rst:2275 +#: ../../api.rst:2368 msgid "An invite was updated." msgstr "招待の更新。" -#: ../../api.rst:2277 +#: ../../api.rst:2370 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was updated." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` の型は更新された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2282 +#: ../../api.rst:2375 msgid "An invite was deleted." msgstr "招待の削除。" -#: ../../api.rst:2284 +#: ../../api.rst:2377 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Invite` that was deleted." msgstr "これが上記のactionならば、:attr:`~AuditLogEntry.target` のタイプは削除された招待に該当する :class:`Invite` になります。" -#: ../../api.rst:2299 +#: ../../api.rst:2392 msgid "A webhook was created." msgstr "Webhookの作成。" -#: ../../api.rst:2301 -#: ../../api.rst:2317 -#: ../../api.rst:2330 +#: ../../api.rst:2394 +#: ../../api.rst:2410 +#: ../../api.rst:2423 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Object` with the webhook ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のタイプは、webhook IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2308 -#: ../../api.rst:2337 +#: ../../api.rst:2401 +#: ../../api.rst:2430 msgid ":attr:`~AuditLogDiff.type` (always set to ``1`` if so)" msgstr ":attr:`~AuditLogDiff.type` (その場合は常に ``1`` です。)" -#: ../../api.rst:2312 +#: ../../api.rst:2405 msgid "A webhook was updated. This trigger in the following situations:" msgstr "Webhookの更新。これのトリガーとなるのは以下の場合です。" -#: ../../api.rst:2314 +#: ../../api.rst:2407 msgid "The webhook name changed" msgstr "Webhook名が変更されたとき" -#: ../../api.rst:2315 +#: ../../api.rst:2408 msgid "The webhook channel changed" msgstr "Webhookチャンネルが変更されたとき" -#: ../../api.rst:2324 +#: ../../api.rst:2417 msgid ":attr:`~AuditLogDiff.avatar`" msgstr ":attr:`~AuditLogDiff.avatar`" -#: ../../api.rst:2328 +#: ../../api.rst:2421 msgid "A webhook was deleted." msgstr "Webhookの削除。" -#: ../../api.rst:2341 +#: ../../api.rst:2434 msgid "An emoji was created." msgstr "絵文字の作成。" -#: ../../api.rst:2343 -#: ../../api.rst:2354 +#: ../../api.rst:2436 +#: ../../api.rst:2447 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Emoji` or :class:`Object` with the emoji ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Emoji` または 絵文字IDが設定された :class:`Object` です。" -#: ../../api.rst:2352 +#: ../../api.rst:2445 msgid "An emoji was updated. This triggers when the name has changed." msgstr "絵文字に対する何らかの更新。これは名前が変更されたときに発生します。" -#: ../../api.rst:2363 +#: ../../api.rst:2456 msgid "An emoji was deleted." msgstr "絵文字の削除。" -#: ../../api.rst:2365 +#: ../../api.rst:2458 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Object` with the emoji ID." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、絵文字IDが設定されている :class:`Object` になります。" -#: ../../api.rst:2374 +#: ../../api.rst:2467 msgid "A message was deleted by a moderator. Note that this only triggers if the message was deleted by someone other than the author." msgstr "管理者によるメッセージの削除。なお、これのトリガーとなるのは、メッセージが投稿者以外によって削除された場合のみです。" -#: ../../api.rst:2377 +#: ../../api.rst:2470 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、削除されたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2383 -#: ../../api.rst:2396 +#: ../../api.rst:2476 +#: ../../api.rst:2489 msgid "``count``: An integer specifying how many messages were deleted." msgstr "``count`` : 削除されたメッセージの数を示す整数。" -#: ../../api.rst:2384 +#: ../../api.rst:2477 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message got deleted." msgstr "``channel`` : メッセージが削除された :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2388 +#: ../../api.rst:2481 msgid "Messages were bulk deleted by a moderator." msgstr "管理者によるメッセージの一括削除。" -#: ../../api.rst:2390 +#: ../../api.rst:2483 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`TextChannel` or :class:`Object` with the ID of the channel that was purged." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`TextChannel` またはメッセージが一括削除されたチャンネルIDが設定された :class:`Object` です。" -#: ../../api.rst:2402 +#: ../../api.rst:2495 msgid "A message was pinned in a channel." msgstr "チャンネルへのメッセージのピン留め。" -#: ../../api.rst:2404 +#: ../../api.rst:2497 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message pinned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ピン留めされたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2410 +#: ../../api.rst:2503 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message was pinned." msgstr "``channel`` : メッセージがピン留めされた :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2411 +#: ../../api.rst:2504 msgid "``message_id``: the ID of the message which was pinned." msgstr "``message_id`` : ピン留めされたメッセージのID。" -#: ../../api.rst:2417 +#: ../../api.rst:2510 msgid "A message was unpinned in a channel." msgstr "チャンネルからのメッセージのピン留め解除。" -#: ../../api.rst:2419 +#: ../../api.rst:2512 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Member`, :class:`User`, or :class:`Object` who had their message unpinned." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、ピン留めが外されたメッセージの送信者である :class:`Member` 、 :class:`User` 、または :class:`Object` になります。" -#: ../../api.rst:2425 +#: ../../api.rst:2518 msgid "``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the message was unpinned." msgstr "``channel`` : メッセージのピン留めが外された :class:`TextChannel` か チャンネルIDを持つ :class:`Object` 。" -#: ../../api.rst:2426 +#: ../../api.rst:2519 msgid "``message_id``: the ID of the message which was unpinned." msgstr "``message_id`` : ピン留めが外されたメッセージのID。" -#: ../../api.rst:2432 +#: ../../api.rst:2525 msgid "A guild integration was created." msgstr "ギルドの連携サービスの作成。" -#: ../../api.rst:2434 +#: ../../api.rst:2527 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または作成されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2442 +#: ../../api.rst:2535 msgid "A guild integration was updated." msgstr "ギルド連携サービスの更新。" -#: ../../api.rst:2444 +#: ../../api.rst:2537 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または更新されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2452 +#: ../../api.rst:2545 msgid "A guild integration was deleted." msgstr "ギルド連携サービスの削除。" -#: ../../api.rst:2454 +#: ../../api.rst:2547 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` or :class:`Object` with the integration ID of the integration which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`PartialIntegration` または削除されたインテグレーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2462 +#: ../../api.rst:2555 msgid "A stage instance was started." msgstr "ステージインスタンスの開始。" -#: ../../api.rst:2464 +#: ../../api.rst:2557 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`StageInstance` or :class:`Object` with the ID of the stage instance which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`StageInstance` または作成されたステージインスタンスのIDが設定された :class:`Object` です。" -#: ../../api.rst:2471 -#: ../../api.rst:2486 +#: ../../api.rst:2564 +#: ../../api.rst:2579 msgid ":attr:`~AuditLogDiff.privacy_level`" msgstr ":attr:`~AuditLogDiff.privacy_level`" -#: ../../api.rst:2477 +#: ../../api.rst:2570 msgid "A stage instance was updated." msgstr "ステージインスタンスの更新。" -#: ../../api.rst:2479 +#: ../../api.rst:2572 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`StageInstance` or :class:`Object` with the ID of the stage instance which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`StageInstance` または更新されたステージインスタンスのIDが設定された :class:`Object` です。" -#: ../../api.rst:2492 +#: ../../api.rst:2585 msgid "A stage instance was ended." msgstr "ステージインスタンスの終了。" -#: ../../api.rst:2498 +#: ../../api.rst:2591 msgid "A sticker was created." msgstr "スタンプの作成。" -#: ../../api.rst:2500 +#: ../../api.rst:2593 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`GuildSticker` or :class:`Object` with the ID of the sticker which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`GuildSticker` または作成されたスタンプのIDが設定された :class:`Object` です。" -#: ../../api.rst:2507 -#: ../../api.rst:2526 -#: ../../api.rst:2545 +#: ../../api.rst:2600 +#: ../../api.rst:2619 +#: ../../api.rst:2638 msgid ":attr:`~AuditLogDiff.emoji`" msgstr ":attr:`~AuditLogDiff.emoji`" -#: ../../api.rst:2509 -#: ../../api.rst:2528 -#: ../../api.rst:2547 +#: ../../api.rst:2602 +#: ../../api.rst:2621 +#: ../../api.rst:2640 msgid ":attr:`~AuditLogDiff.format_type`" msgstr ":attr:`~AuditLogDiff.format_type`" -#: ../../api.rst:2511 -#: ../../api.rst:2530 -#: ../../api.rst:2549 +#: ../../api.rst:2604 +#: ../../api.rst:2623 +#: ../../api.rst:2642 msgid ":attr:`~AuditLogDiff.available`" msgstr ":attr:`~AuditLogDiff.available`" -#: ../../api.rst:2517 +#: ../../api.rst:2610 msgid "A sticker was updated." msgstr "スタンプの更新。" -#: ../../api.rst:2519 -#: ../../api.rst:2538 +#: ../../api.rst:2612 +#: ../../api.rst:2631 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`GuildSticker` or :class:`Object` with the ID of the sticker which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`GuildSticker` または更新されたスタンプのIDが設定された :class:`Object` です。" -#: ../../api.rst:2536 +#: ../../api.rst:2629 msgid "A sticker was deleted." msgstr "スタンプの削除。" -#: ../../api.rst:2555 -#: ../../api.rst:2574 -#: ../../api.rst:2593 +#: ../../api.rst:2648 +#: ../../api.rst:2667 +#: ../../api.rst:2686 msgid "A scheduled event was created." msgstr "スケジュールイベントの作成。" -#: ../../api.rst:2557 +#: ../../api.rst:2650 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または作成されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2561 -#: ../../api.rst:2580 -#: ../../api.rst:2599 +#: ../../api.rst:2654 +#: ../../api.rst:2673 +#: ../../api.rst:2692 msgid "Possible attributes for :class:`AuditLogDiff`: - :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.description` - :attr:`~AuditLogDiff.privacy_level` - :attr:`~AuditLogDiff.status` - :attr:`~AuditLogDiff.entity_type` - :attr:`~AuditLogDiff.cover_image`" msgstr ":class:`AuditLogDiff` の可能な属性: - :attr:`~AuditLogDiff.name` - :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.description` - :attr:`~AuditLogDiff.privacy_level` - :attr:`~AuditLogDiff.status` - :attr:`~AuditLogDiff.entity_type` - :attr:`~AuditLogDiff.cover_image`" -#: ../../api.rst:2576 +#: ../../api.rst:2669 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または更新されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2595 +#: ../../api.rst:2688 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`ScheduledEvent` or :class:`Object` with the ID of the event which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`ScheduledEvent` または削除されたスケジュールイベントのIDが設定された :class:`Object` です。" -#: ../../api.rst:2612 +#: ../../api.rst:2705 msgid "A thread was created." msgstr "スレッドの作成。" -#: ../../api.rst:2614 +#: ../../api.rst:2707 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または作成されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2621 -#: ../../api.rst:2639 -#: ../../api.rst:2657 +#: ../../api.rst:2714 +#: ../../api.rst:2732 +#: ../../api.rst:2750 msgid ":attr:`~AuditLogDiff.archived`" msgstr ":attr:`~AuditLogDiff.archived`" -#: ../../api.rst:2622 -#: ../../api.rst:2640 -#: ../../api.rst:2658 +#: ../../api.rst:2715 +#: ../../api.rst:2733 +#: ../../api.rst:2751 msgid ":attr:`~AuditLogDiff.locked`" msgstr ":attr:`~AuditLogDiff.locked`" -#: ../../api.rst:2623 -#: ../../api.rst:2641 -#: ../../api.rst:2659 +#: ../../api.rst:2716 +#: ../../api.rst:2734 +#: ../../api.rst:2752 msgid ":attr:`~AuditLogDiff.auto_archive_duration`" msgstr ":attr:`~AuditLogDiff.auto_archive_duration`" -#: ../../api.rst:2624 -#: ../../api.rst:2642 -#: ../../api.rst:2660 +#: ../../api.rst:2717 +#: ../../api.rst:2735 +#: ../../api.rst:2753 msgid ":attr:`~AuditLogDiff.invitable`" msgstr ":attr:`~AuditLogDiff.invitable`" -#: ../../api.rst:2630 +#: ../../api.rst:2723 msgid "A thread was updated." msgstr "スレッドの更新。" -#: ../../api.rst:2632 +#: ../../api.rst:2725 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または更新されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2648 +#: ../../api.rst:2741 msgid "A thread was deleted." msgstr "スレッドの削除。" -#: ../../api.rst:2650 +#: ../../api.rst:2743 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is the :class:`Thread` or :class:`Object` with the ID of the thread which was deleted." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 :class:`Thread` または削除されたスレッドのIDが設定された :class:`Object` です。" -#: ../../api.rst:2666 +#: ../../api.rst:2759 msgid "An application command or integrations application command permissions were updated." msgstr "アプリケーションコマンドまたはインテグレーションアプリケーションコマンドの権限の更新。" -#: ../../api.rst:2669 +#: ../../api.rst:2762 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`PartialIntegration` for an integrations general permissions, :class:`~discord.app_commands.AppCommand` for a specific commands permissions, or :class:`Object` with the ID of the command or integration which was updated." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` の型は、 インテグレーション一般の権限の場合 :class:`PartialIntegration` 、特定のコマンドの権限の場合 :class:`~discord.app_commands.AppCommand` 、あるいは更新されたコマンドまたはインテグレーションのIDが設定された :class:`Object` です。" -#: ../../api.rst:2675 +#: ../../api.rst:2768 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an :class:`PartialIntegration` or :class:`Object` with the ID of application that command or integration belongs to." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` の型は、 :class:`PartialIntegration` またはコマンドまたはインテグレーションが属するアプリケーションのIDを持つ :class:`Object` になります。" -#: ../../api.rst:2681 +#: ../../api.rst:2774 msgid ":attr:`~AuditLogDiff.app_command_permissions`" msgstr ":attr:`~AuditLogDiff.app_command_permissions`" -#: ../../api.rst:2687 +#: ../../api.rst:2780 msgid "An automod rule was created." msgstr "自動管理ルールの作成。" -#: ../../api.rst:2689 -#: ../../api.rst:2710 -#: ../../api.rst:2731 +#: ../../api.rst:2782 +#: ../../api.rst:2803 +#: ../../api.rst:2824 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`AutoModRule` or :class:`Object` with the ID of the automod rule that was created." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のtypeは、 :class:`AutoModRule` または作成された自動管理ルールのIDが設定された :class:`Object` です。" -#: ../../api.rst:2696 -#: ../../api.rst:2717 -#: ../../api.rst:2738 +#: ../../api.rst:2789 +#: ../../api.rst:2810 +#: ../../api.rst:2831 msgid ":attr:`~AuditLogDiff.enabled`" msgstr ":attr:`~AuditLogDiff.enabled`" -#: ../../api.rst:2697 -#: ../../api.rst:2718 -#: ../../api.rst:2739 +#: ../../api.rst:2790 +#: ../../api.rst:2811 +#: ../../api.rst:2832 msgid ":attr:`~AuditLogDiff.event_type`" msgstr ":attr:`~AuditLogDiff.event_type`" -#: ../../api.rst:2698 -#: ../../api.rst:2719 -#: ../../api.rst:2740 +#: ../../api.rst:2791 +#: ../../api.rst:2812 +#: ../../api.rst:2833 msgid ":attr:`~AuditLogDiff.trigger_type`" msgstr ":attr:`~AuditLogDiff.trigger_type`" -#: ../../api.rst:2699 -#: ../../api.rst:2720 -#: ../../api.rst:2741 +#: ../../api.rst:2792 +#: ../../api.rst:2813 +#: ../../api.rst:2834 msgid ":attr:`~AuditLogDiff.trigger`" msgstr ":attr:`~AuditLogDiff.trigger`" -#: ../../api.rst:2700 -#: ../../api.rst:2721 -#: ../../api.rst:2742 +#: ../../api.rst:2793 +#: ../../api.rst:2814 +#: ../../api.rst:2835 msgid ":attr:`~AuditLogDiff.actions`" msgstr ":attr:`~AuditLogDiff.actions`" -#: ../../api.rst:2701 -#: ../../api.rst:2722 -#: ../../api.rst:2743 +#: ../../api.rst:2794 +#: ../../api.rst:2815 +#: ../../api.rst:2836 msgid ":attr:`~AuditLogDiff.exempt_roles`" msgstr ":attr:`~AuditLogDiff.exempt_roles`" -#: ../../api.rst:2702 -#: ../../api.rst:2723 -#: ../../api.rst:2744 +#: ../../api.rst:2795 +#: ../../api.rst:2816 +#: ../../api.rst:2837 msgid ":attr:`~AuditLogDiff.exempt_channels`" msgstr ":attr:`~AuditLogDiff.exempt_channels`" -#: ../../api.rst:2708 +#: ../../api.rst:2801 msgid "An automod rule was updated." msgstr "自動管理ルールの更新。" -#: ../../api.rst:2729 +#: ../../api.rst:2822 msgid "An automod rule was deleted." msgstr "自動管理ルールの削除。" -#: ../../api.rst:2750 +#: ../../api.rst:2843 msgid "An automod rule blocked a message from being sent." msgstr "自動管理ルールによる送信されたメッセージのブロック。" -#: ../../api.rst:2752 -#: ../../api.rst:2770 -#: ../../api.rst:2788 +#: ../../api.rst:2845 +#: ../../api.rst:2863 +#: ../../api.rst:2881 msgid "When this is the action, the type of :attr:`~AuditLogEntry.target` is a :class:`Member` with the ID of the person who triggered the automod rule." msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.target` のtypeは、自動管理ルールを発動させた :class:`Member` になります。" -#: ../../api.rst:2755 -#: ../../api.rst:2773 -#: ../../api.rst:2791 +#: ../../api.rst:2848 +#: ../../api.rst:2866 +#: ../../api.rst:2884 msgid "When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with 3 attributes:" msgstr "これが上記のactionならば、 :attr:`~AuditLogEntry.extra` は以下の3つの属性を持つプロキシオブジェクトになります:" -#: ../../api.rst:2758 -#: ../../api.rst:2776 -#: ../../api.rst:2794 +#: ../../api.rst:2851 +#: ../../api.rst:2869 +#: ../../api.rst:2887 msgid "``automod_rule_name``: The name of the automod rule that was triggered." msgstr "``automod_rule_name`` : 発動した自動管理ルールの名前。" -#: ../../api.rst:2759 -#: ../../api.rst:2777 -#: ../../api.rst:2795 -msgid "``automod_rule_trigger``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered." -msgstr "``automod_rule_trigger`` : 発動されたルールの :class:`AutoModRuleTriggerType` 。" +#: ../../api.rst:2852 +#: ../../api.rst:2870 +#: ../../api.rst:2888 +msgid "``automod_rule_trigger_type``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered." +msgstr "" -#: ../../api.rst:2760 -#: ../../api.rst:2778 -#: ../../api.rst:2796 +#: ../../api.rst:2853 +#: ../../api.rst:2871 +#: ../../api.rst:2889 msgid "``channel``: The channel in which the automod rule was triggered." msgstr "``channel`` : 自動管理ルールが発動されたチャンネル。" -#: ../../api.rst:2762 -#: ../../api.rst:2780 -#: ../../api.rst:2798 +#: ../../api.rst:2855 +#: ../../api.rst:2873 +#: ../../api.rst:2891 msgid "When this is the action, :attr:`AuditLogEntry.changes` is empty." msgstr "これが上記のactionなら、 :attr:`~AuditLogEntry.changes` は空になります。" -#: ../../api.rst:2768 +#: ../../api.rst:2861 msgid "An automod rule flagged a message." msgstr "自動管理ルールによる送信されたメッセージのフラグ付け。" -#: ../../api.rst:2786 +#: ../../api.rst:2879 msgid "An automod rule timed-out a member." msgstr "自動管理ルールによるメンバーのタイムアウト。" -#: ../../api.rst:2804 +#: ../../api.rst:2897 +msgid "A request to monetize the server was created." +msgstr "" + +#: ../../api.rst:2903 +msgid "The terms and conditions for creator monetization were accepted." +msgstr "" + +#: ../../api.rst:2909 msgid "Represents the category that the :class:`AuditLogAction` belongs to." msgstr ":class:`AuditLogAction` が属するカテゴリ。" -#: ../../api.rst:2806 +#: ../../api.rst:2911 msgid "This can be retrieved via :attr:`AuditLogEntry.category`." msgstr "これは :attr:`AuditLogEntry.category` で取得できます。" -#: ../../api.rst:2810 +#: ../../api.rst:2915 msgid "The action is the creation of something." msgstr "アクションは何かの作成です。" -#: ../../api.rst:2814 +#: ../../api.rst:2919 msgid "The action is the deletion of something." msgstr "アクションは何かの削除です。" -#: ../../api.rst:2818 +#: ../../api.rst:2923 msgid "The action is the update of something." msgstr "アクションは何かの更新です。" -#: ../../api.rst:2822 +#: ../../api.rst:2927 msgid "Represents the membership state of a team member retrieved through :func:`Client.application_info`." msgstr ":func:`Client.application_info` で取得したチームメンバーのメンバーシップ状態。" -#: ../../api.rst:2828 +#: ../../api.rst:2933 msgid "Represents an invited member." msgstr "招待されたメンバー。" -#: ../../api.rst:2832 +#: ../../api.rst:2937 msgid "Represents a member currently in the team." msgstr "現在チームにいるメンバー。" -#: ../../api.rst:2836 +#: ../../api.rst:2941 +msgid "Represents the type of role of a team member retrieved through :func:`Client.application_info`." +msgstr "" + +#: ../../api.rst:2947 +msgid "The team member is an admin. This allows them to invite members to the team, access credentials, edit the application, and do most things the owner can do. However they cannot do destructive actions." +msgstr "" + +#: ../../api.rst:2952 +msgid "The team member is a developer. This allows them to access information, like the client secret or public key. They can also configure interaction endpoints or reset the bot token. Developers cannot invite anyone to the team nor can they do destructive actions." +msgstr "" + +#: ../../api.rst:2958 +msgid "The team member is a read-only member. This allows them to access information, but not edit anything." +msgstr "" + +#: ../../api.rst:2962 msgid "Represents the type of webhook that can be received." msgstr "受け取れるWebhookの種類。" -#: ../../api.rst:2842 +#: ../../api.rst:2968 msgid "Represents a webhook that can post messages to channels with a token." msgstr "トークンでチャンネルにメッセージを投稿できるWebhook。" -#: ../../api.rst:2846 +#: ../../api.rst:2972 msgid "Represents a webhook that is internally managed by Discord, used for following channels." msgstr "チャンネルのフォローのためDiscord内部で管理されるWebhook。" -#: ../../api.rst:2850 +#: ../../api.rst:2976 msgid "Represents a webhook that is used for interactions or applications." msgstr "インタラクションやアプリケーションに用いられるWebhook。" -#: ../../api.rst:2856 +#: ../../api.rst:2982 msgid "Represents the behaviour the :class:`Integration` should perform when a user's subscription has finished." msgstr "ユーザーのサブスクリプションが終了した後の :class:`Integration` の動作。" -#: ../../api.rst:2859 +#: ../../api.rst:2985 msgid "There is an alias for this called ``ExpireBehavior``." msgstr "``ExpireBehavior`` という名のエイリアスがあります。" -#: ../../api.rst:2865 +#: ../../api.rst:2991 msgid "This will remove the :attr:`StreamIntegration.role` from the user when their subscription is finished." msgstr "サブスクリプションが終了したユーザーから :attr:`StreamIntegration.role` を除去します。" -#: ../../api.rst:2870 +#: ../../api.rst:2996 msgid "This will kick the user when their subscription is finished." msgstr "サブスクリプションが終了したユーザーをキックします。" -#: ../../api.rst:2874 +#: ../../api.rst:3000 msgid "Represents the default avatar of a Discord :class:`User`" msgstr "Discord :class:`User` のデフォルトのアバター。" -#: ../../api.rst:2878 +#: ../../api.rst:3004 msgid "Represents the default avatar with the colour blurple. See also :attr:`Colour.blurple`" msgstr "ブループル色のデフォルトのアバター。 :attr:`Colour.blurple` も参照してください。" -#: ../../api.rst:2882 +#: ../../api.rst:3008 msgid "Represents the default avatar with the colour grey. See also :attr:`Colour.greyple`" msgstr "灰色のデフォルトのアバター。 :attr:`Colour.greyple` も参照してください。" -#: ../../api.rst:2886 +#: ../../api.rst:3012 msgid "An alias for :attr:`grey`." msgstr ":attr:`grey` のエイリアス。" -#: ../../api.rst:2889 +#: ../../api.rst:3015 msgid "Represents the default avatar with the colour green. See also :attr:`Colour.green`" msgstr "緑色のデフォルトのアバター。 :attr:`Colour.green` も参照してください。" -#: ../../api.rst:2893 +#: ../../api.rst:3019 msgid "Represents the default avatar with the colour orange. See also :attr:`Colour.orange`" msgstr "オレンジ色のデフォルトのアバター。 :attr:`Colour.orange` も参照してください。" -#: ../../api.rst:2897 +#: ../../api.rst:3023 msgid "Represents the default avatar with the colour red. See also :attr:`Colour.red`" msgstr "赤色のデフォルトのアバター。 :attr:`Colour.red` も参照してください。" -#: ../../api.rst:2901 +#: ../../api.rst:3027 msgid "Represents the default avatar with the colour pink. See also :attr:`Colour.pink`" msgstr "ピンク色のデフォルトのアバター。 :attr:`Colour.pink` も参照してください。" -#: ../../api.rst:2908 +#: ../../api.rst:3034 msgid "Represents the type of sticker." msgstr "スタンプの種類。" -#: ../../api.rst:2914 +#: ../../api.rst:3040 msgid "Represents a standard sticker that all Nitro users can use." msgstr "Nitroユーザー全員が使用できる標準スタンプ。" -#: ../../api.rst:2918 +#: ../../api.rst:3044 msgid "Represents a custom sticker created in a guild." msgstr "ギルドで作成されたカスタムスタンプ。" -#: ../../api.rst:2922 +#: ../../api.rst:3048 msgid "Represents the type of sticker images." msgstr "スタンプ画像の種類。" -#: ../../api.rst:2928 +#: ../../api.rst:3054 msgid "Represents a sticker with a png image." msgstr "PNG画像のスタンプ。" -#: ../../api.rst:2932 +#: ../../api.rst:3058 msgid "Represents a sticker with an apng image." msgstr "APNG画像のスタンプ。" -#: ../../api.rst:2936 +#: ../../api.rst:3062 msgid "Represents a sticker with a lottie image." msgstr "ロッティー画像のスタンプ。" -#: ../../api.rst:2940 +#: ../../api.rst:3066 msgid "Represents a sticker with a gif image." msgstr "GIF画像のスタンプ。" -#: ../../api.rst:2946 +#: ../../api.rst:3072 msgid "Represents the invite type for voice channel invites." msgstr "ボイスチャンネル招待の招待タイプ。" -#: ../../api.rst:2952 +#: ../../api.rst:3078 msgid "The invite doesn't target anyone or anything." msgstr "招待の対象がないもの。" -#: ../../api.rst:2956 +#: ../../api.rst:3082 msgid "A stream invite that targets a user." msgstr "ユーザーを対象とするもの。" -#: ../../api.rst:2960 +#: ../../api.rst:3086 msgid "A stream invite that targets an embedded application." msgstr "埋め込まれたアプリケーションを対象とするもの。" -#: ../../api.rst:2964 +#: ../../api.rst:3090 msgid "Represents the camera video quality mode for voice channel participants." msgstr "ボイスチャンネル参加者のカメラビデオの画質モード。" -#: ../../api.rst:2970 +#: ../../api.rst:3096 msgid "Represents auto camera video quality." msgstr "自動のカメラビデオ画質。" -#: ../../api.rst:2974 +#: ../../api.rst:3100 msgid "Represents full camera video quality." msgstr "フルのカメラビデオ画質。" -#: ../../api.rst:2978 +#: ../../api.rst:3104 msgid "Represents the privacy level of a stage instance or scheduled event." msgstr "ステージインスタンスやスケジュールイベントのプライバシーレベル。" -#: ../../api.rst:2984 +#: ../../api.rst:3110 msgid "The stage instance or scheduled event is only accessible within the guild." msgstr "ステージインスタンスやスケジュールイベントはギルド内でのみアクセスできます。" -#: ../../api.rst:2988 +#: ../../api.rst:3114 msgid "Represents the NSFW level of a guild." msgstr "ギルドの年齢制限レベル。" -#: ../../api.rst:2996 +#: ../../api.rst:3122 msgid "Checks if two NSFW levels are equal." msgstr "二つの年齢制限レベルが等しいかを比較します。" -#: ../../api.rst:2999 +#: ../../api.rst:3125 msgid "Checks if two NSFW levels are not equal." msgstr "二つの年齢制限レベルが等しくないかを比較します。" -#: ../../api.rst:3002 +#: ../../api.rst:3128 msgid "Checks if a NSFW level is higher than another." msgstr "年齢制限レベルがあるレベルより高いか確認します。" -#: ../../api.rst:3005 +#: ../../api.rst:3131 msgid "Checks if a NSFW level is lower than another." msgstr "年齢制限レベルがあるレベルより低いか確認します。" -#: ../../api.rst:3008 +#: ../../api.rst:3134 msgid "Checks if a NSFW level is higher or equal to another." msgstr "年齢制限レベルがあるレベルと同じ、又は高いか確認します。" -#: ../../api.rst:3011 +#: ../../api.rst:3137 msgid "Checks if a NSFW level is lower or equal to another." msgstr "年齢制限レベルがあるレベルと同じ、又は低いか確認します。" -#: ../../api.rst:3015 +#: ../../api.rst:3141 msgid "The guild has not been categorised yet." msgstr "未分類のギルド。" -#: ../../api.rst:3019 +#: ../../api.rst:3145 msgid "The guild contains NSFW content." msgstr "年齢制限されたコンテンツを含むギルド。" -#: ../../api.rst:3023 +#: ../../api.rst:3149 msgid "The guild does not contain any NSFW content." msgstr "年齢制限されたコンテンツを一切含まないギルド。" -#: ../../api.rst:3027 +#: ../../api.rst:3153 msgid "The guild may contain NSFW content." msgstr "年齢制限されたコンテンツを含む可能性のあるギルド。" -#: ../../api.rst:3031 +#: ../../api.rst:3157 msgid "Supported locales by Discord. Mainly used for application command localisation." msgstr "Discordでサポートされているロケール。主にアプリケーションコマンドの多言語化に使用されます。" -#: ../../api.rst:3037 +#: ../../api.rst:3163 msgid "The ``en-US`` locale." msgstr "``en-US`` ロケール。" -#: ../../api.rst:3041 +#: ../../api.rst:3167 msgid "The ``en-GB`` locale." msgstr "``en-GB`` ロケール。" -#: ../../api.rst:3045 +#: ../../api.rst:3171 msgid "The ``bg`` locale." msgstr "``bg`` ロケール。" -#: ../../api.rst:3049 +#: ../../api.rst:3175 msgid "The ``zh-CN`` locale." msgstr "``zh-CN`` ロケール。" -#: ../../api.rst:3053 +#: ../../api.rst:3179 msgid "The ``zh-TW`` locale." msgstr "``zh-TW`` ロケール。" -#: ../../api.rst:3057 +#: ../../api.rst:3183 msgid "The ``hr`` locale." msgstr "``hr`` ロケール。" -#: ../../api.rst:3061 +#: ../../api.rst:3187 msgid "The ``cs`` locale." msgstr "``cs`` ロケール。" -#: ../../api.rst:3065 +#: ../../api.rst:3191 msgid "The ``id`` locale." msgstr "``id`` ロケール。" -#: ../../api.rst:3071 +#: ../../api.rst:3197 msgid "The ``da`` locale." msgstr "``da`` ロケール。" -#: ../../api.rst:3075 +#: ../../api.rst:3201 msgid "The ``nl`` locale." msgstr "``nl`` ロケール。" -#: ../../api.rst:3079 +#: ../../api.rst:3205 msgid "The ``fi`` locale." msgstr "``fi`` ロケール。" -#: ../../api.rst:3083 +#: ../../api.rst:3209 msgid "The ``fr`` locale." msgstr "``fr`` ロケール。" -#: ../../api.rst:3087 +#: ../../api.rst:3213 msgid "The ``de`` locale." msgstr "``de`` ロケール。" -#: ../../api.rst:3091 +#: ../../api.rst:3217 msgid "The ``el`` locale." msgstr "``el`` ロケール。" -#: ../../api.rst:3095 +#: ../../api.rst:3221 msgid "The ``hi`` locale." msgstr "``hi`` ロケール。" -#: ../../api.rst:3099 +#: ../../api.rst:3225 msgid "The ``hu`` locale." msgstr "``hu`` ロケール。" -#: ../../api.rst:3103 +#: ../../api.rst:3229 msgid "The ``it`` locale." msgstr "``it`` ロケール。" -#: ../../api.rst:3107 +#: ../../api.rst:3233 msgid "The ``ja`` locale." msgstr "``ja`` ロケール。" -#: ../../api.rst:3111 +#: ../../api.rst:3237 msgid "The ``ko`` locale." msgstr "``ko`` ロケール。" -#: ../../api.rst:3115 +#: ../../api.rst:3241 +msgid "The ``es-419`` locale." +msgstr "" + +#: ../../api.rst:3247 msgid "The ``lt`` locale." msgstr "``lt`` ロケール。" -#: ../../api.rst:3119 +#: ../../api.rst:3251 msgid "The ``no`` locale." msgstr "``no`` ロケール。" -#: ../../api.rst:3123 +#: ../../api.rst:3255 msgid "The ``pl`` locale." msgstr "``pl`` ロケール。" -#: ../../api.rst:3127 +#: ../../api.rst:3259 msgid "The ``pt-BR`` locale." msgstr "``pt-BR`` ロケール。" -#: ../../api.rst:3131 +#: ../../api.rst:3263 msgid "The ``ro`` locale." msgstr "``ro`` ロケール。" -#: ../../api.rst:3135 +#: ../../api.rst:3267 msgid "The ``ru`` locale." msgstr "``ru`` ロケール。" -#: ../../api.rst:3139 +#: ../../api.rst:3271 msgid "The ``es-ES`` locale." msgstr "``es-ES`` ロケール。" -#: ../../api.rst:3143 +#: ../../api.rst:3275 msgid "The ``sv-SE`` locale." msgstr "``sv-SE`` ロケール。" -#: ../../api.rst:3147 +#: ../../api.rst:3279 msgid "The ``th`` locale." msgstr "``th`` ロケール。" -#: ../../api.rst:3151 +#: ../../api.rst:3283 msgid "The ``tr`` locale." msgstr "``tr`` ロケール。" -#: ../../api.rst:3155 +#: ../../api.rst:3287 msgid "The ``uk`` locale." msgstr "``uk`` ロケール。" -#: ../../api.rst:3159 +#: ../../api.rst:3291 msgid "The ``vi`` locale." msgstr "``vi`` ロケール。" -#: ../../api.rst:3164 +#: ../../api.rst:3296 msgid "Represents the Multi-Factor Authentication requirement level of a guild." msgstr "ギルドの多要素認証要件レベル。" -#: ../../api.rst:3172 +#: ../../api.rst:3304 msgid "Checks if two MFA levels are equal." msgstr "二つのMFAレベルが等しいかを比較します。" -#: ../../api.rst:3175 +#: ../../api.rst:3307 msgid "Checks if two MFA levels are not equal." msgstr "二つのMFAレベルが等しくないかを比較します。" -#: ../../api.rst:3178 +#: ../../api.rst:3310 msgid "Checks if a MFA level is higher than another." msgstr "多要素認証レベルがあるレベルより厳しいか確認します。" -#: ../../api.rst:3181 +#: ../../api.rst:3313 msgid "Checks if a MFA level is lower than another." msgstr "多要素認証レベルがあるレベルより緩いか確認します。" -#: ../../api.rst:3184 +#: ../../api.rst:3316 msgid "Checks if a MFA level is higher or equal to another." msgstr "多要素認証レベルがあるレベルと同じ、又は厳しいか確認します。" -#: ../../api.rst:3187 +#: ../../api.rst:3319 msgid "Checks if a MFA level is lower or equal to another." msgstr "多要素認証レベルがあるレベルと同じ、又は緩いか確認します。" -#: ../../api.rst:3191 +#: ../../api.rst:3323 msgid "The guild has no MFA requirement." msgstr "多要素認証要件がないギルド。" -#: ../../api.rst:3195 +#: ../../api.rst:3327 msgid "The guild requires 2 factor authentication." msgstr "二要素認証を必須とするギルド。" -#: ../../api.rst:3199 +#: ../../api.rst:3331 msgid "Represents the type of entity that a scheduled event is for." msgstr "スケジュールイベントの開催場所の種類。" -#: ../../api.rst:3205 +#: ../../api.rst:3337 msgid "The scheduled event will occur in a stage instance." msgstr "ステージインスタンスで起こるスケジュールイベント。" -#: ../../api.rst:3209 +#: ../../api.rst:3341 msgid "The scheduled event will occur in a voice channel." msgstr "ボイスチャンネルで起こるスケジュールイベント。" -#: ../../api.rst:3213 +#: ../../api.rst:3345 msgid "The scheduled event will occur externally." msgstr "外部で起こるスケジュールイベント。" -#: ../../api.rst:3217 +#: ../../api.rst:3349 msgid "Represents the status of an event." msgstr "イベントの状態。" -#: ../../api.rst:3223 +#: ../../api.rst:3355 msgid "The event is scheduled." msgstr "予定されたイベント。" -#: ../../api.rst:3227 +#: ../../api.rst:3359 msgid "The event is active." msgstr "開催中のイベント。" -#: ../../api.rst:3231 +#: ../../api.rst:3363 msgid "The event has ended." msgstr "終了したイベント。" -#: ../../api.rst:3235 +#: ../../api.rst:3367 msgid "The event has been cancelled." msgstr "キャンセルされたイベント。" -#: ../../api.rst:3239 +#: ../../api.rst:3371 msgid "An alias for :attr:`cancelled`." msgstr ":attr:`cancelled` のエイリアス。" -#: ../../api.rst:3243 +#: ../../api.rst:3375 msgid "An alias for :attr:`completed`." msgstr ":attr:`completed` のエイリアス。" -#: ../../api.rst:3247 +#: ../../api.rst:3379 msgid "Represents the trigger type of an automod rule." msgstr "自動管理ルールの発動条件の種類を表します。" -#: ../../api.rst:3253 +#: ../../api.rst:3385 msgid "The rule will trigger when a keyword is mentioned." msgstr "キーワードに言及したときに発動されるルール。" -#: ../../api.rst:3257 +#: ../../api.rst:3389 msgid "The rule will trigger when a harmful link is posted." msgstr "有害なリンクを投稿したときに発動されるルール。" -#: ../../api.rst:3261 +#: ../../api.rst:3393 msgid "The rule will trigger when a spam message is posted." msgstr "スパムメッセージを投稿したときに発動されるルール。" -#: ../../api.rst:3265 +#: ../../api.rst:3397 msgid "The rule will trigger when something triggers based on the set keyword preset types." msgstr "事前に定められたキーワードプリセットに基づき発動したときに発動されるルール。" -#: ../../api.rst:3269 +#: ../../api.rst:3401 msgid "The rule will trigger when combined number of role and user mentions is greater than the set limit." msgstr "ロールとユーザーのメンションの合計数が設定された制限よりも多い場合に発動されるルール。" -#: ../../api.rst:3274 +#: ../../api.rst:3406 +msgid "The rule will trigger when a user's profile contains a keyword." +msgstr "" + +#: ../../api.rst:3412 msgid "Represents the event type of an automod rule." msgstr "自動管理ルールのイベントの種類を表します。" -#: ../../api.rst:3280 +#: ../../api.rst:3418 msgid "The rule will trigger when a message is sent." msgstr "メッセージを投稿したときにルールが発動します。" -#: ../../api.rst:3284 +#: ../../api.rst:3422 +msgid "The rule will trigger when a member's profile is updated." +msgstr "" + +#: ../../api.rst:3428 msgid "Represents the action type of an automod rule." msgstr "自動管理ルールの対応の種類を表します。" -#: ../../api.rst:3290 +#: ../../api.rst:3434 msgid "The rule will block a message from being sent." msgstr "メッセージを送信できないようにします。" -#: ../../api.rst:3294 +#: ../../api.rst:3438 msgid "The rule will send an alert message to a predefined channel." msgstr "事前に指定したチャンネルに警告メッセージを送信します。" -#: ../../api.rst:3298 +#: ../../api.rst:3442 msgid "The rule will timeout a user." msgstr "ユーザーをタイムアウトします。" -#: ../../api.rst:3303 +#: ../../api.rst:3446 +msgid "Similar to :attr:`timeout`, except the user will be timed out indefinitely. This will request the user to edit it's profile." +msgstr "" + +#: ../../api.rst:3453 msgid "Represents how a forum's posts are layed out in the client." msgstr "フォーラムの投稿がクライアントでどのように配列されるかを表します。" -#: ../../api.rst:3309 +#: ../../api.rst:3459 msgid "No default has been set, so it is up to the client to know how to lay it out." msgstr "デフォルトが設定されていないので、配列方法はクライアントによります。" -#: ../../api.rst:3313 +#: ../../api.rst:3463 msgid "Displays posts as a list." msgstr "投稿を一覧として表示します。" -#: ../../api.rst:3317 +#: ../../api.rst:3467 msgid "Displays posts as a collection of tiles." msgstr "投稿をタイルの集まりとして表示します。" -#: ../../api.rst:3322 +#: ../../api.rst:3472 msgid "Represents how a forum's posts are sorted in the client." msgstr "フォーラムの投稿がクライアントでどのように並び替えられるかを表します。" -#: ../../api.rst:3328 +#: ../../api.rst:3478 msgid "Sort forum posts by activity." msgstr "最終更新日時順でフォーラム投稿を並び替えます。" -#: ../../api.rst:3332 +#: ../../api.rst:3482 msgid "Sort forum posts by creation time (from most recent to oldest)." msgstr "作成日時順 (新しいものから古いものの順) でフォーラム投稿を並び替えます。" -#: ../../api.rst:3338 +#: ../../api.rst:3486 +msgid "Represents the default value of a select menu." +msgstr "" + +#: ../../api.rst:3492 +msgid "The underlying type of the ID is a user." +msgstr "" + +#: ../../api.rst:3496 +msgid "The underlying type of the ID is a role." +msgstr "" + +#: ../../api.rst:3500 +msgid "The underlying type of the ID is a channel or thread." +msgstr "" + +#: ../../api.rst:3505 +msgid "Represents the type of a SKU." +msgstr "" + +#: ../../api.rst:3511 +msgid "The SKU is a recurring subscription." +msgstr "" + +#: ../../api.rst:3515 +msgid "The SKU is a system-generated group which is created for each :attr:`SKUType.subscription`." +msgstr "" + +#: ../../api.rst:3520 +msgid "Represents the type of an entitlement." +msgstr "" + +#: ../../api.rst:3526 +msgid "The entitlement was purchased as an app subscription." +msgstr "" + +#: ../../api.rst:3531 +msgid "Represents the type of an entitlement owner." +msgstr "" + +#: ../../api.rst:3537 +msgid "The entitlement owner is a guild." +msgstr "" + +#: ../../api.rst:3541 +msgid "The entitlement owner is a user." +msgstr "" + +#: ../../api.rst:3547 msgid "Audit Log Data" msgstr "監査ログデータ" -#: ../../api.rst:3340 +#: ../../api.rst:3549 msgid "Working with :meth:`Guild.audit_logs` is a complicated process with a lot of machinery involved. The library attempts to make it easy to use and friendly. In order to accomplish this goal, it must make use of a couple of data classes that aid in this goal." msgstr ":meth:`Guild.audit_logs` の使用は複雑なプロセスです。このライブラリーはこれを使いやすくフレンドリーにしようと試みています。この目標の達成のためにいくつかのデータクラスを使用しています。" -#: ../../api.rst:3345 +#: ../../api.rst:3554 msgid "AuditLogEntry" msgstr "AuditLogEntry" @@ -6406,406 +6877,406 @@ msgstr ":class:`AuditLogDiff`" msgid "The target's subsequent state." msgstr "対象の直後の状態。" -#: ../../api.rst:3353 +#: ../../api.rst:3562 msgid "AuditLogChanges" msgstr "AuditLogChanges" -#: ../../api.rst:3359 +#: ../../api.rst:3568 msgid "An audit log change set." msgstr "監査ログの変更のセット。" -#: ../../api.rst:3363 +#: ../../api.rst:3572 msgid "The old value. The attribute has the type of :class:`AuditLogDiff`." msgstr "以前の値。この属性は :class:`AuditLogDiff` 型です。" -#: ../../api.rst:3365 -#: ../../api.rst:3385 +#: ../../api.rst:3574 +#: ../../api.rst:3594 msgid "Depending on the :class:`AuditLogActionCategory` retrieved by :attr:`~AuditLogEntry.category`\\, the data retrieved by this attribute differs:" msgstr ":attr:`~AuditLogEntry.category` で取得される :class:`AuditLogActionCategory` によりこの属性の値が異なります:" -#: ../../api.rst:3370 -#: ../../api.rst:3390 +#: ../../api.rst:3579 +#: ../../api.rst:3599 msgid "Category" msgstr "カテゴリー" -#: ../../api.rst:3372 -#: ../../api.rst:3392 +#: ../../api.rst:3581 +#: ../../api.rst:3601 msgid ":attr:`~AuditLogActionCategory.create`" msgstr ":attr:`~AuditLogActionCategory.create`" -#: ../../api.rst:3372 +#: ../../api.rst:3581 msgid "All attributes are set to ``None``." msgstr "全ての属性は ``None`` です。" -#: ../../api.rst:3374 -#: ../../api.rst:3394 +#: ../../api.rst:3583 +#: ../../api.rst:3603 msgid ":attr:`~AuditLogActionCategory.delete`" msgstr ":attr:`~AuditLogActionCategory.delete`" -#: ../../api.rst:3374 +#: ../../api.rst:3583 msgid "All attributes are set the value before deletion." msgstr "全ての属性は削除前の値に設定されています。" -#: ../../api.rst:3376 -#: ../../api.rst:3396 +#: ../../api.rst:3585 +#: ../../api.rst:3605 msgid ":attr:`~AuditLogActionCategory.update`" msgstr ":attr:`~AuditLogActionCategory.update`" -#: ../../api.rst:3376 +#: ../../api.rst:3585 msgid "All attributes are set the value before updating." msgstr "全ての属性は更新前の値に設定されています。" -#: ../../api.rst:3378 -#: ../../api.rst:3398 +#: ../../api.rst:3587 +#: ../../api.rst:3607 msgid "``None``" msgstr "``None``" -#: ../../api.rst:3378 -#: ../../api.rst:3398 +#: ../../api.rst:3587 +#: ../../api.rst:3607 msgid "No attributes are set." msgstr "属性が設定されていません。" -#: ../../api.rst:3383 +#: ../../api.rst:3592 msgid "The new value. The attribute has the type of :class:`AuditLogDiff`." msgstr "新しい値。この属性は :class:`AuditLogDiff` 型です。" -#: ../../api.rst:3392 +#: ../../api.rst:3601 msgid "All attributes are set to the created value" msgstr "全ての属性は作成時の値に設定されています。" -#: ../../api.rst:3394 +#: ../../api.rst:3603 msgid "All attributes are set to ``None``" msgstr "全ての属性は ``None`` です。" -#: ../../api.rst:3396 +#: ../../api.rst:3605 msgid "All attributes are set the value after updating." msgstr "全ての属性は更新後の値に設定されています。" -#: ../../api.rst:3402 +#: ../../api.rst:3611 msgid "AuditLogDiff" msgstr "AuditLogDiff" -#: ../../api.rst:3408 +#: ../../api.rst:3617 msgid "Represents an audit log \"change\" object. A change object has dynamic attributes that depend on the type of action being done. Certain actions map to certain attributes being set." msgstr "監査ログの「変更」オブジェクト。変更オブジェクトには、行われたアクションの種類によって異なる属性があります。特定のアクションが行われた場合に特定の属性が設定されます。" -#: ../../api.rst:3412 +#: ../../api.rst:3621 msgid "Note that accessing an attribute that does not match the specified action will lead to an attribute error." msgstr "指定されたアクションに一致しない属性にアクセスすると、AttributeErrorが発生することに注意してください。" -#: ../../api.rst:3415 +#: ../../api.rst:3624 msgid "To get a list of attributes that have been set, you can iterate over them. To see a list of all possible attributes that could be set based on the action being done, check the documentation for :class:`AuditLogAction`, otherwise check the documentation below for all attributes that are possible." msgstr "設定された属性のリストを取得するには、イテレートすることができます。 行われたアクションに対応した可能な属性の一覧は、 :class:`AuditLogAction` の説明を確認してください。あるいは、可能なすべての属性について、以下の説明を確認してください。" -#: ../../api.rst:3424 +#: ../../api.rst:3633 msgid "Returns an iterator over (attribute, value) tuple of this diff." msgstr "差分の(属性、値)タプルのイテレーターを返します。" -#: ../../api.rst:3428 +#: ../../api.rst:3637 msgid "A name of something." msgstr "何かの名前。" -#: ../../api.rst:3434 +#: ../../api.rst:3643 msgid "The guild of something." msgstr "ギルド属性。" -#: ../../api.rst:3440 +#: ../../api.rst:3649 msgid "A guild's or role's icon. See also :attr:`Guild.icon` or :attr:`Role.icon`." msgstr "ギルドまたはロールのアイコン。 :attr:`Guild.icon` と :attr:`Role.icon` も参照してください。" -#: ../../api.rst:3446 +#: ../../api.rst:3655 msgid "The guild's invite splash. See also :attr:`Guild.splash`." msgstr "ギルドの招待のスプラッシュ。 :attr:`Guild.splash` も参照してください。" -#: ../../api.rst:3452 +#: ../../api.rst:3661 msgid "The guild's discovery splash. See also :attr:`Guild.discovery_splash`." msgstr "ギルドのディスカバリースプラッシュ。 :attr:`Guild.discovery_splash` も参照してください。" -#: ../../api.rst:3458 +#: ../../api.rst:3667 msgid "The guild's banner. See also :attr:`Guild.banner`." msgstr "ギルドのバナー。 :attr:`Guild.banner` も参照してください。" -#: ../../api.rst:3464 +#: ../../api.rst:3673 msgid "The guild's owner. See also :attr:`Guild.owner`" msgstr "ギルドの所有者。 :attr:`Guild.owner` も参照してください。" -#: ../../api.rst:3466 +#: ../../api.rst:3675 msgid "Union[:class:`Member`, :class:`User`]" msgstr "Union[:class:`Member`, :class:`User`]" -#: ../../api.rst:3470 +#: ../../api.rst:3679 msgid "The guild's AFK channel." msgstr "ギルドのAFKチャンネル。" -#: ../../api.rst:3472 -#: ../../api.rst:3483 +#: ../../api.rst:3681 +#: ../../api.rst:3692 msgid "If this could not be found, then it falls back to a :class:`Object` with the ID being set." msgstr "見つからない場合は、IDが設定された :class:`Object` になります。" -#: ../../api.rst:3475 +#: ../../api.rst:3684 msgid "See :attr:`Guild.afk_channel`." msgstr ":attr:`Guild.afk_channel` を参照してください。" -#: ../../api.rst:3477 +#: ../../api.rst:3686 msgid "Union[:class:`VoiceChannel`, :class:`Object`]" msgstr "Union[:class:`VoiceChannel`, :class:`Object`]" -#: ../../api.rst:3481 +#: ../../api.rst:3690 msgid "The guild's system channel." msgstr "ギルドのシステムチャンネル。" -#: ../../api.rst:3486 +#: ../../api.rst:3695 msgid "See :attr:`Guild.system_channel`." msgstr ":attr:`Guild.system_channel` を参照してください。" -#: ../../api.rst:3488 -#: ../../api.rst:3500 -#: ../../api.rst:3512 -#: ../../api.rst:3539 +#: ../../api.rst:3697 +#: ../../api.rst:3709 +#: ../../api.rst:3721 +#: ../../api.rst:3748 msgid "Union[:class:`TextChannel`, :class:`Object`]" msgstr "Union[:class:`TextChannel`, :class:`Object`]" -#: ../../api.rst:3493 +#: ../../api.rst:3702 msgid "The guild's rules channel." msgstr "ギルドのルールチャンネル。" -#: ../../api.rst:3495 -#: ../../api.rst:3507 -#: ../../api.rst:3536 +#: ../../api.rst:3704 +#: ../../api.rst:3716 +#: ../../api.rst:3745 msgid "If this could not be found then it falls back to a :class:`Object` with the ID being set." msgstr "見つからない場合は、IDが設定された :class:`Object` になります。" -#: ../../api.rst:3498 +#: ../../api.rst:3707 msgid "See :attr:`Guild.rules_channel`." msgstr ":attr:`Guild.rules_channel` を参照してください。" -#: ../../api.rst:3505 +#: ../../api.rst:3714 msgid "The guild's public updates channel." msgstr "ギルドのパブリックアップデートチャンネル。" -#: ../../api.rst:3510 +#: ../../api.rst:3719 msgid "See :attr:`Guild.public_updates_channel`." msgstr ":attr:`Guild.public_updates_channel` を参照してください。" -#: ../../api.rst:3516 +#: ../../api.rst:3725 msgid "The guild's AFK timeout. See :attr:`Guild.afk_timeout`." msgstr "ギルドのAFKタイムアウト。 :attr:`Guild.afk_timeout` も参照してください。" -#: ../../api.rst:3522 +#: ../../api.rst:3731 msgid "The guild's MFA level. See :attr:`Guild.mfa_level`." msgstr "ギルドの多要素認証レベル。 :attr:`Guild.mfa_level` も参照してください。" -#: ../../api.rst:3524 +#: ../../api.rst:3733 #: ../../../discord/guild.py:docstring of discord.guild.Guild:173 msgid ":class:`MFALevel`" msgstr ":class:`MFALevel`" -#: ../../api.rst:3528 +#: ../../api.rst:3737 msgid "The guild's widget has been enabled or disabled." msgstr "ギルドのウィジェットが有効化または無効化された。" -#: ../../api.rst:3534 +#: ../../api.rst:3743 msgid "The widget's channel." msgstr "ウィジェットのチャンネル。" -#: ../../api.rst:3543 +#: ../../api.rst:3752 #: ../../../discord/guild.py:docstring of discord.guild.Guild:103 msgid "The guild's verification level." msgstr "ギルドの認証レベル。" -#: ../../api.rst:3545 +#: ../../api.rst:3754 msgid "See also :attr:`Guild.verification_level`." msgstr ":attr:`Guild.verification_level` も参照してください。" -#: ../../api.rst:3547 +#: ../../api.rst:3756 #: ../../../discord/guild.py:docstring of discord.guild.Guild:105 #: ../../../discord/invite.py:docstring of discord.invite.PartialInviteGuild:40 msgid ":class:`VerificationLevel`" msgstr ":class:`VerificationLevel`" -#: ../../api.rst:3551 +#: ../../api.rst:3760 msgid "The guild's default notification level." msgstr "ギルドのデフォルト通知レベル。" -#: ../../api.rst:3553 +#: ../../api.rst:3762 msgid "See also :attr:`Guild.default_notifications`." msgstr ":attr:`Guild.default_notifications` も参照してください。" -#: ../../api.rst:3555 +#: ../../api.rst:3764 #: ../../../discord/guild.py:docstring of discord.guild.Guild:125 msgid ":class:`NotificationLevel`" msgstr ":class:`NotificationLevel`" -#: ../../api.rst:3559 +#: ../../api.rst:3768 msgid "The guild's content filter." msgstr "ギルドのコンテンツフィルター。" -#: ../../api.rst:3561 +#: ../../api.rst:3770 msgid "See also :attr:`Guild.explicit_content_filter`." msgstr ":attr:`Guild.explicit_content_filter` も参照してください。" -#: ../../api.rst:3563 +#: ../../api.rst:3772 #: ../../../discord/guild.py:docstring of discord.guild.Guild:119 msgid ":class:`ContentFilter`" msgstr ":class:`ContentFilter`" -#: ../../api.rst:3567 +#: ../../api.rst:3776 msgid "The guild's vanity URL." msgstr "ギルドのバニティURL。" -#: ../../api.rst:3569 +#: ../../api.rst:3778 msgid "See also :meth:`Guild.vanity_invite` and :meth:`Guild.edit`." msgstr ":meth:`Guild.vanity_invite` と :meth:`Guild.edit` も参照してください。" -#: ../../api.rst:3575 +#: ../../api.rst:3784 msgid "The position of a :class:`Role` or :class:`abc.GuildChannel`." msgstr ":class:`Role` や :class:`abc.GuildChannel` の位置。" -#: ../../api.rst:3581 +#: ../../api.rst:3790 msgid "The type of channel, sticker, webhook or integration." msgstr "チャンネル、スタンプ、Webhookまたは連携サービスのタイプ。" -#: ../../api.rst:3583 +#: ../../api.rst:3792 msgid "Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`]" msgstr "Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`]" -#: ../../api.rst:3587 +#: ../../api.rst:3796 msgid "The topic of a :class:`TextChannel` or :class:`StageChannel`." msgstr ":class:`TextChannel` または :class:`StageChannel` のトピック。" -#: ../../api.rst:3589 +#: ../../api.rst:3798 msgid "See also :attr:`TextChannel.topic` or :attr:`StageChannel.topic`." msgstr ":attr:`TextChannel.topic` または :attr:`StageChannel.topic` も参照してください。" -#: ../../api.rst:3595 +#: ../../api.rst:3804 msgid "The bitrate of a :class:`VoiceChannel`." msgstr ":class:`VoiceChannel` のビットレート。" -#: ../../api.rst:3597 +#: ../../api.rst:3806 msgid "See also :attr:`VoiceChannel.bitrate`." msgstr ":attr:`VoiceChannel.bitrate` も参照してください。" -#: ../../api.rst:3603 +#: ../../api.rst:3812 msgid "A list of permission overwrite tuples that represents a target and a :class:`PermissionOverwrite` for said target." msgstr "対象とその :class:`PermissionOverwrite` のタプルで示された権限の上書きのリスト。" -#: ../../api.rst:3606 +#: ../../api.rst:3815 msgid "The first element is the object being targeted, which can either be a :class:`Member` or :class:`User` or :class:`Role`. If this object is not found then it is a :class:`Object` with an ID being filled and a ``type`` attribute set to either ``'role'`` or ``'member'`` to help decide what type of ID it is." msgstr "最初の要素は対象のオブジェクトで、 :class:`Member` か :class:`User` か :class:`Role` です。このオブジェクトが見つからない場合はこれはIDが設定され、 ``type`` 属性が ``'role'`` か ``'member'`` に設定された :class:`Object` になります。" -#: ../../api.rst:3612 +#: ../../api.rst:3821 msgid "List[Tuple[target, :class:`PermissionOverwrite`]]" msgstr "List[Tuple[target, :class:`PermissionOverwrite`]]" -#: ../../api.rst:3616 +#: ../../api.rst:3825 msgid "The privacy level of the stage instance or scheduled event" msgstr "ステージインスタンスやスケジュールイベントのプライバシーレベル。" -#: ../../api.rst:3618 +#: ../../api.rst:3827 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:65 #: ../../../discord/stage_instance.py:docstring of discord.stage_instance.StageInstance:47 msgid ":class:`PrivacyLevel`" msgstr ":class:`PrivacyLevel`" -#: ../../api.rst:3622 +#: ../../api.rst:3831 msgid "A list of roles being added or removed from a member." msgstr "メンバーから追加または削除されたロールのリスト。" -#: ../../api.rst:3624 +#: ../../api.rst:3833 msgid "If a role is not found then it is a :class:`Object` with the ID and name being filled in." msgstr "ロールが見つからない場合は、IDとnameが設定された :class:`Object` になります。" -#: ../../api.rst:3627 -#: ../../api.rst:3968 +#: ../../api.rst:3836 +#: ../../api.rst:4183 msgid "List[Union[:class:`Role`, :class:`Object`]]" msgstr "List[Union[:class:`Role`, :class:`Object`]]" -#: ../../api.rst:3631 +#: ../../api.rst:3840 msgid "The nickname of a member." msgstr "メンバーのニックネーム。" -#: ../../api.rst:3633 +#: ../../api.rst:3842 msgid "See also :attr:`Member.nick`" msgstr ":attr:`Member.nick` も参照してください。" -#: ../../api.rst:3639 +#: ../../api.rst:3848 msgid "Whether the member is being server deafened." msgstr "メンバーがサーバーでスピーカーミュートされているかどうか。" -#: ../../api.rst:3641 +#: ../../api.rst:3850 msgid "See also :attr:`VoiceState.deaf`." msgstr ":attr:`VoiceState.deaf` も参照してください。" -#: ../../api.rst:3647 +#: ../../api.rst:3856 msgid "Whether the member is being server muted." msgstr "メンバーがサーバーでミュートされているかどうか。" -#: ../../api.rst:3649 +#: ../../api.rst:3858 msgid "See also :attr:`VoiceState.mute`." msgstr ":attr:`VoiceState.mute` も参照してください。" -#: ../../api.rst:3655 +#: ../../api.rst:3864 msgid "The permissions of a role." msgstr "ロールの権限。" -#: ../../api.rst:3657 +#: ../../api.rst:3866 msgid "See also :attr:`Role.permissions`." msgstr ":attr:`Role.permissions` も参照してください。" -#: ../../api.rst:3664 +#: ../../api.rst:3873 msgid "The colour of a role." msgstr "ロールの色。" -#: ../../api.rst:3666 +#: ../../api.rst:3875 msgid "See also :attr:`Role.colour`" msgstr ":attr:`Role.colour` も参照してください。" -#: ../../api.rst:3672 +#: ../../api.rst:3881 msgid "Whether the role is being hoisted or not." msgstr "役割が別に表示されるかどうか。" -#: ../../api.rst:3674 +#: ../../api.rst:3883 msgid "See also :attr:`Role.hoist`" msgstr ":attr:`Role.hoist` も参照してください。" -#: ../../api.rst:3680 +#: ../../api.rst:3889 msgid "Whether the role is mentionable or not." msgstr "役割がメンションできるかどうか。" -#: ../../api.rst:3682 +#: ../../api.rst:3891 msgid "See also :attr:`Role.mentionable`" msgstr ":attr:`Role.mentionable` も参照してください。" -#: ../../api.rst:3688 +#: ../../api.rst:3897 msgid "The invite's code." msgstr "招待のコード。" -#: ../../api.rst:3690 +#: ../../api.rst:3899 msgid "See also :attr:`Invite.code`" msgstr ":attr:`Invite.code` も参照してください。" -#: ../../api.rst:3696 +#: ../../api.rst:3905 msgid "A guild channel." msgstr "ギルドのチャンネル。" -#: ../../api.rst:3698 +#: ../../api.rst:3907 msgid "If the channel is not found then it is a :class:`Object` with the ID being set. In some cases the channel name is also set." msgstr "チャンネルが見つからない場合は、IDが設定された :class:`Object` になります。 場合によっては、チャンネル名も設定されています。" -#: ../../api.rst:3701 +#: ../../api.rst:3910 msgid "Union[:class:`abc.GuildChannel`, :class:`Object`]" msgstr "Union[:class:`abc.GuildChannel`, :class:`Object`]" -#: ../../api.rst:3705 +#: ../../api.rst:3914 #: ../../../discord/invite.py:docstring of discord.invite.Invite:101 msgid "The user who created the invite." msgstr "招待を作成したユーザー。" -#: ../../api.rst:3707 +#: ../../api.rst:3916 msgid "See also :attr:`Invite.inviter`." msgstr ":attr:`Invite.inviter` も参照してください。" -#: ../../api.rst:3709 +#: ../../api.rst:3918 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:83 #: ../../../discord/integrations.py:docstring of discord.integrations.IntegrationApplication:39 #: ../../../discord/emoji.py:docstring of discord.emoji.Emoji:76 @@ -6813,71 +7284,71 @@ msgstr ":attr:`Invite.inviter` も参照してください。" msgid "Optional[:class:`User`]" msgstr "Optional[:class:`User`]" -#: ../../api.rst:3713 +#: ../../api.rst:3922 msgid "The invite's max uses." msgstr "招待の最大使用回数。" -#: ../../api.rst:3715 +#: ../../api.rst:3924 msgid "See also :attr:`Invite.max_uses`." msgstr ":attr:`Invite.max_uses` も参照してください。" -#: ../../api.rst:3721 +#: ../../api.rst:3930 msgid "The invite's current uses." msgstr "招待の現在の使用回数。" -#: ../../api.rst:3723 +#: ../../api.rst:3932 msgid "See also :attr:`Invite.uses`." msgstr ":attr:`Invite.uses` も参照してください。" -#: ../../api.rst:3729 +#: ../../api.rst:3938 msgid "The invite's max age in seconds." msgstr "招待者の最大時間は秒数です。" -#: ../../api.rst:3731 +#: ../../api.rst:3940 msgid "See also :attr:`Invite.max_age`." msgstr ":attr:`Invite.max_age` も参照してください。" -#: ../../api.rst:3737 +#: ../../api.rst:3946 msgid "If the invite is a temporary invite." msgstr "招待が一時的な招待であるか。" -#: ../../api.rst:3739 +#: ../../api.rst:3948 msgid "See also :attr:`Invite.temporary`." msgstr ":attr:`Invite.temporary` も参照してください。" -#: ../../api.rst:3746 +#: ../../api.rst:3955 msgid "The permissions being allowed or denied." msgstr "許可または拒否された権限。" -#: ../../api.rst:3752 +#: ../../api.rst:3961 msgid "The ID of the object being changed." msgstr "変更されたオブジェクトのID。" -#: ../../api.rst:3758 +#: ../../api.rst:3967 msgid "The avatar of a member." msgstr "メンバーのアバター。" -#: ../../api.rst:3760 +#: ../../api.rst:3969 msgid "See also :attr:`User.avatar`." msgstr ":attr:`User.avatar` も参照してください。" -#: ../../api.rst:3766 +#: ../../api.rst:3975 msgid "The number of seconds members have to wait before sending another message in the channel." msgstr "メンバーが別のメッセージをチャンネルに送信するまでの秒単位の待ち時間。" -#: ../../api.rst:3769 +#: ../../api.rst:3978 msgid "See also :attr:`TextChannel.slowmode_delay`." msgstr ":attr:`TextChannel.slowmode_delay` も参照してください。" -#: ../../api.rst:3775 +#: ../../api.rst:3984 msgid "The region for the voice channel’s voice communication. A value of ``None`` indicates automatic voice region detection." msgstr "ボイスチャンネルの音声通信のためのリージョン。値が ``None`` の場合は自動で検出されます。" -#: ../../api.rst:3778 +#: ../../api.rst:3987 msgid "See also :attr:`VoiceChannel.rtc_region`." msgstr ":attr:`VoiceChannel.rtc_region` も参照してください。" -#: ../../api.rst:3784 +#: ../../api.rst:3993 #: ../../../discord/guild.py:docstring of discord.guild.Guild.create_voice_channel:31 #: ../../../discord/guild.py:docstring of discord.guild.Guild.create_stage_channel:37 #: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel:86 @@ -6885,25 +7356,25 @@ msgstr ":attr:`VoiceChannel.rtc_region` も参照してください。" msgid "The camera video quality for the voice channel's participants." msgstr "ボイスチャンネル参加者のカメラビデオの画質。" -#: ../../api.rst:3786 +#: ../../api.rst:3995 msgid "See also :attr:`VoiceChannel.video_quality_mode`." msgstr ":attr:`VoiceChannel.video_quality_mode` も参照してください。" -#: ../../api.rst:3788 +#: ../../api.rst:3997 #: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel:90 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel:93 msgid ":class:`VideoQualityMode`" msgstr ":class:`VideoQualityMode`" -#: ../../api.rst:3792 +#: ../../api.rst:4001 msgid "The format type of a sticker being changed." msgstr "変更されたスタンプのフォーマットの種類。" -#: ../../api.rst:3794 +#: ../../api.rst:4003 msgid "See also :attr:`GuildSticker.format`" msgstr ":attr:`GuildSticker.format` も参照してください。" -#: ../../api.rst:3796 +#: ../../api.rst:4005 #: ../../../discord/sticker.py:docstring of discord.sticker.StickerItem:35 #: ../../../discord/sticker.py:docstring of discord.sticker.Sticker:47 #: ../../../discord/sticker.py:docstring of discord.sticker.StandardSticker:47 @@ -6911,306 +7382,315 @@ msgstr ":attr:`GuildSticker.format` も参照してください。" msgid ":class:`StickerFormatType`" msgstr ":class:`StickerFormatType`" -#: ../../api.rst:3800 +#: ../../api.rst:4009 msgid "The name of the emoji that represents a sticker being changed." msgstr "変更されたスタンプを示す絵文字の名前。" -#: ../../api.rst:3802 +#: ../../api.rst:4011 msgid "See also :attr:`GuildSticker.emoji`." msgstr ":attr:`GuildSticker.emoji` も参照してください。" -#: ../../api.rst:3808 +#: ../../api.rst:4017 msgid "The unicode emoji that is used as an icon for the role being changed." msgstr "変更されたロールのアイコンとして使用されるUnicode絵文字。" -#: ../../api.rst:3810 +#: ../../api.rst:4019 msgid "See also :attr:`Role.unicode_emoji`." msgstr ":attr:`Role.unicode_emoji` も参照してください。" -#: ../../api.rst:3816 +#: ../../api.rst:4025 msgid "The description of a guild, a sticker, or a scheduled event." msgstr "ギルド、スタンプ、またはスケジュールイベントの説明。" -#: ../../api.rst:3818 +#: ../../api.rst:4027 msgid "See also :attr:`Guild.description`, :attr:`GuildSticker.description`, or :attr:`ScheduledEvent.description`." msgstr ":attr:`Guild.description` 、 :attr:`GuildSticker.description` 、または :attr:`ScheduledEvent.description` も参照してください。" -#: ../../api.rst:3825 +#: ../../api.rst:4034 msgid "The availability of a sticker being changed." msgstr "変更されたスタンプの利用可能かどうかの状態。" -#: ../../api.rst:3827 +#: ../../api.rst:4036 msgid "See also :attr:`GuildSticker.available`" msgstr ":attr:`GuildSticker.available` も参照してください。" -#: ../../api.rst:3833 +#: ../../api.rst:4042 msgid "The thread is now archived." msgstr "スレッドがアーカイブされたか。" -#: ../../api.rst:3839 +#: ../../api.rst:4048 msgid "The thread is being locked or unlocked." msgstr "スレッドがロックされ、またはロックが解除されたかどうか。" -#: ../../api.rst:3845 +#: ../../api.rst:4054 msgid "The thread's auto archive duration being changed." msgstr "変更されたスレッドの自動アーカイブ期間。" -#: ../../api.rst:3847 +#: ../../api.rst:4056 msgid "See also :attr:`Thread.auto_archive_duration`" msgstr ":attr:`Thread.auto_archive_duration` も参照してください。" -#: ../../api.rst:3853 +#: ../../api.rst:4062 msgid "The default auto archive duration for newly created threads being changed." msgstr "変更された新規作成されたスレッドの既定の自動アーカイブ期間。" -#: ../../api.rst:3859 +#: ../../api.rst:4068 msgid "Whether non-moderators can add users to this private thread." msgstr "モデレータ以外がプライベートスレッドにユーザーを追加できるかどうか。" -#: ../../api.rst:3865 +#: ../../api.rst:4074 msgid "Whether the user is timed out, and if so until when." msgstr "ユーザーがタイムアウトされているかどうか、そしてその場合はいつまでか。" -#: ../../api.rst:3867 +#: ../../api.rst:4076 #: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.edited_at:3 #: ../../../discord/message.py:docstring of discord.Message.edited_at:3 -#: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:59 -#: ../../../discord/member.py:docstring of discord.member.Member:30 +#: ../../../discord/guild.py:docstring of discord.Guild.invites_paused_until:6 +#: ../../../discord/guild.py:docstring of discord.Guild.dms_paused_until:6 msgid "Optional[:class:`datetime.datetime`]" msgstr "Optional[:class:`datetime.datetime`]" -#: ../../api.rst:3871 +#: ../../api.rst:4080 msgid "Integration emoticons were enabled or disabled." msgstr "連携サービスの絵文字が有効化され、または無効化されたか。" -#: ../../api.rst:3873 +#: ../../api.rst:4082 msgid "See also :attr:`StreamIntegration.enable_emoticons`" msgstr ":attr:`StreamIntegration.enable_emoticons` も参照してください。" -#: ../../api.rst:3880 +#: ../../api.rst:4089 msgid "The behaviour of expiring subscribers changed." msgstr "変更された期限切れのサブスクライバーの動作。" -#: ../../api.rst:3882 +#: ../../api.rst:4091 msgid "See also :attr:`StreamIntegration.expire_behaviour`" msgstr ":attr:`StreamIntegration.expire_behaviour` も参照してください。" -#: ../../api.rst:3884 +#: ../../api.rst:4093 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration:51 #: ../../../discord/integrations.py:docstring of discord.StreamIntegration.expire_behavior:3 msgid ":class:`ExpireBehaviour`" msgstr ":class:`ExpireBehaviour`" -#: ../../api.rst:3888 +#: ../../api.rst:4097 msgid "The grace period before expiring subscribers changed." msgstr "変更された期限切れのサブスクライバーの猶予期間。" -#: ../../api.rst:3890 +#: ../../api.rst:4099 msgid "See also :attr:`StreamIntegration.expire_grace_period`" msgstr ":attr:`StreamIntegration.expire_grace_period` も参照してください。" -#: ../../api.rst:3896 +#: ../../api.rst:4105 msgid "The preferred locale for the guild changed." msgstr "変更されたギルドの優先ローケル。" -#: ../../api.rst:3898 +#: ../../api.rst:4107 msgid "See also :attr:`Guild.preferred_locale`" msgstr ":attr:`Guild.preferred_locale` も参照してください。" -#: ../../api.rst:3900 +#: ../../api.rst:4109 #: ../../../discord/guild.py:docstring of discord.guild.Guild:156 msgid ":class:`Locale`" msgstr ":class:`Locale`" -#: ../../api.rst:3904 +#: ../../api.rst:4113 msgid "The number of days after which inactive and role-unassigned members are kicked has been changed." msgstr "変更された活動していない、かつロールが割り当てられていないメンバーがキックさえるまでの日数。" -#: ../../api.rst:3910 +#: ../../api.rst:4119 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:69 msgid "The status of the scheduled event." msgstr "スケジュールイベントのステータス。" -#: ../../api.rst:3912 +#: ../../api.rst:4121 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:71 msgid ":class:`EventStatus`" msgstr ":class:`EventStatus`" -#: ../../api.rst:3916 +#: ../../api.rst:4125 msgid "The type of entity this scheduled event is for." msgstr "スケジュールイベントの開催場所の種類。" -#: ../../api.rst:3918 +#: ../../api.rst:4127 #: ../../../discord/scheduled_event.py:docstring of discord.scheduled_event.ScheduledEvent:41 msgid ":class:`EntityType`" msgstr ":class:`EntityType`" -#: ../../api.rst:3922 +#: ../../api.rst:4131 #: ../../../discord/scheduled_event.py:docstring of discord.ScheduledEvent.cover_image:1 msgid "The scheduled event's cover image." msgstr "スケジュールイベントのカバー画像。" -#: ../../api.rst:3924 +#: ../../api.rst:4133 msgid "See also :attr:`ScheduledEvent.cover_image`." msgstr ":attr:`ScheduledEvent.cover_image` も参照してください。" -#: ../../api.rst:3930 +#: ../../api.rst:4139 msgid "List of permissions for the app command." msgstr "アプリケーションコマンドの権限のリスト。" -#: ../../api.rst:3932 +#: ../../api.rst:4141 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawAppCommandPermissionsUpdateEvent:29 msgid "List[:class:`~discord.app_commands.AppCommandPermissions`]" msgstr "List[:class:`~discord.app_commands.AppCommandPermissions`]" -#: ../../api.rst:3936 +#: ../../api.rst:4145 msgid "Whether the automod rule is active or not." msgstr "自動管理ルールが有効かどうか。" -#: ../../api.rst:3942 +#: ../../api.rst:4151 msgid "The event type for triggering the automod rule." msgstr "自動管理ルールを発動させるイベントの種類。" -#: ../../api.rst:3944 +#: ../../api.rst:4153 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:57 msgid ":class:`AutoModRuleEventType`" msgstr ":class:`AutoModRuleEventType`" -#: ../../api.rst:3948 +#: ../../api.rst:4157 msgid "The trigger type for the automod rule." msgstr "自動管理ルールの発動条件の種類。" -#: ../../api.rst:3950 +#: ../../api.rst:4159 #: ../../../discord/automod.py:docstring of discord.automod.AutoModAction:28 -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:24 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:28 msgid ":class:`AutoModRuleTriggerType`" msgstr ":class:`AutoModRuleTriggerType`" -#: ../../api.rst:3954 +#: ../../api.rst:4163 msgid "The trigger for the automod rule." msgstr "自動管理ルールの発動条件。" -#: ../../api.rst:3956 +#: ../../api.rst:4167 +msgid "The :attr:`~AutoModTrigger.type` of the trigger may be incorrect. Some attributes such as :attr:`~AutoModTrigger.keyword_filter`, :attr:`~AutoModTrigger.regex_patterns`, and :attr:`~AutoModTrigger.allow_list` will only have the added or removed values." +msgstr "" + +#: ../../api.rst:4171 #: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:33 msgid ":class:`AutoModTrigger`" msgstr ":class:`AutoModTrigger`" -#: ../../api.rst:3960 +#: ../../api.rst:4175 msgid "The actions to take when an automod rule is triggered." msgstr "自動管理ルールの発動時の対応。" -#: ../../api.rst:3962 +#: ../../api.rst:4177 msgid "List[AutoModRuleAction]" msgstr "List[AutoModRuleAction]" -#: ../../api.rst:3966 +#: ../../api.rst:4181 msgid "The list of roles that are exempt from the automod rule." msgstr "自動管理ルールの除外対象のロールのリスト。" -#: ../../api.rst:3972 +#: ../../api.rst:4187 msgid "The list of channels or threads that are exempt from the automod rule." msgstr "自動管理ルールの除外対象のチャンネルまたはスレッドのリスト。" -#: ../../api.rst:3974 +#: ../../api.rst:4189 msgid "List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`]" msgstr "List[:class:`abc.GuildChannel`, :class:`Thread`, :class:`Object`]" -#: ../../api.rst:3978 +#: ../../api.rst:4193 msgid "The guild’s display setting to show boost progress bar." msgstr "ギルドのブーストの進捗バーを表示するかの設定。" -#: ../../api.rst:3984 +#: ../../api.rst:4199 msgid "The guild’s system channel settings." msgstr "ギルドのシステムチャンネルの設定。" -#: ../../api.rst:3986 +#: ../../api.rst:4201 msgid "See also :attr:`Guild.system_channel_flags`" msgstr ":attr:`Guild.system_channel_flags` を参照してください。" -#: ../../api.rst:3988 +#: ../../api.rst:4203 #: ../../../discord/guild.py:docstring of discord.Guild.system_channel_flags:3 msgid ":class:`SystemChannelFlags`" msgstr ":class:`SystemChannelFlags`" -#: ../../api.rst:3992 +#: ../../api.rst:4207 msgid "Whether the channel is marked as “not safe for work” or “age restricted”." msgstr "チャンネルに年齢制限がかかっているか。" -#: ../../api.rst:3998 +#: ../../api.rst:4213 msgid "The channel’s limit for number of members that can be in a voice or stage channel." msgstr "ボイスまたはステージチャンネルに参加できるメンバー数の制限。" -#: ../../api.rst:4000 +#: ../../api.rst:4215 msgid "See also :attr:`VoiceChannel.user_limit` and :attr:`StageChannel.user_limit`" msgstr ":attr:`VoiceChannel.user_limit` と :attr:`StageChannel.user_limit` も参照してください。" -#: ../../api.rst:4006 +#: ../../api.rst:4221 msgid "The channel flags associated with this thread or forum post." msgstr "このスレッドやフォーラム投稿に関連付けられたチャンネルフラグ。" -#: ../../api.rst:4008 +#: ../../api.rst:4223 msgid "See also :attr:`ForumChannel.flags` and :attr:`Thread.flags`" msgstr ":attr:`ForumChannel.flags` と :attr:`Thread.flags` も参照してください。" -#: ../../api.rst:4010 +#: ../../api.rst:4225 #: ../../../discord/channel.py:docstring of discord.ForumChannel.flags:5 #: ../../../discord/threads.py:docstring of discord.Thread.flags:3 msgid ":class:`ChannelFlags`" msgstr ":class:`ChannelFlags`" -#: ../../api.rst:4014 +#: ../../api.rst:4229 msgid "The default slowmode delay for threads created in this text channel or forum." msgstr "このテキストチャンネルやフォーラムで作成されたスレッドのデフォルトの低速モードのレート制限。" -#: ../../api.rst:4016 +#: ../../api.rst:4231 msgid "See also :attr:`TextChannel.default_thread_slowmode_delay` and :attr:`ForumChannel.default_thread_slowmode_delay`" msgstr ":attr:`TextChannel.default_thread_slowmode_delay` と :attr:`ForumChannel.default_thread_slowmode_delay` も参照してください。" -#: ../../api.rst:4022 +#: ../../api.rst:4237 msgid "The applied tags of a forum post." msgstr "フォーラム投稿に適用されたタグ。" -#: ../../api.rst:4024 +#: ../../api.rst:4239 msgid "See also :attr:`Thread.applied_tags`" msgstr ":attr:`Thread.applied_tags` も参照してください。" -#: ../../api.rst:4026 +#: ../../api.rst:4241 msgid "List[Union[:class:`ForumTag`, :class:`Object`]]" msgstr "List[Union[:class:`ForumTag`, :class:`Object`]]" -#: ../../api.rst:4030 +#: ../../api.rst:4245 msgid "The available tags of a forum." msgstr "フォーラムにて利用可能なタグ。" -#: ../../api.rst:4032 +#: ../../api.rst:4247 msgid "See also :attr:`ForumChannel.available_tags`" msgstr ":attr:`ForumChannel.available_tags` も参照してください。" -#: ../../api.rst:4034 +#: ../../api.rst:4249 #: ../../../discord/channel.py:docstring of discord.ForumChannel.available_tags:5 msgid "Sequence[:class:`ForumTag`]" msgstr "Sequence[:class:`ForumTag`]" -#: ../../api.rst:4038 +#: ../../api.rst:4253 msgid "The default_reaction_emoji for forum posts." msgstr "フォーラム投稿の default_reaction_emoji。" -#: ../../api.rst:4040 +#: ../../api.rst:4255 msgid "See also :attr:`ForumChannel.default_reaction_emoji`" msgstr ":attr:`ForumChannel.default_reaction_emoji` も参照してください。" -#: ../../api.rst:4042 -msgid ":class:`default_reaction_emoji`" -msgstr ":class:`default_reaction_emoji`" +#: ../../api.rst:4257 +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:105 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:100 +#: ../../../discord/activity.py:docstring of discord.activity.CustomActivity:33 +#: ../../../discord/channel.py:docstring of discord.channel.ForumTag:48 +msgid "Optional[:class:`PartialEmoji`]" +msgstr "Optional[:class:`PartialEmoji`]" -#: ../../api.rst:4048 +#: ../../api.rst:4263 msgid "Webhook Support" msgstr "Webhookサポート" -#: ../../api.rst:4050 +#: ../../api.rst:4265 msgid "discord.py offers support for creating, editing, and executing webhooks through the :class:`Webhook` class." msgstr "discord.pyは、 :class:`Webhook` クラスからWebhookの作成、編集、実行をサポートします。" -#: ../../api.rst:4053 +#: ../../api.rst:4268 msgid "Webhook" msgstr "Webhook" @@ -7666,7 +8146,11 @@ msgstr "メッセージの埋め込みを抑制するかどうか。これが `` msgid "Whether to suppress push and desktop notifications for the message. This will increment the mention counter in the UI, but will not actually send a notification." msgstr "メッセージのプッシュ通知とデスクトップ通知を抑制するかどうか。 抑制した場合、UIのメンションカウンターを増やしますが、実際に通知を送信することはありません。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:84 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:83 +msgid "Tags to apply to the thread if the webhook belongs to a :class:`~discord.ForumChannel`." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:88 #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.reply:12 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:63 #: ../../../discord/abc.py:docstring of discord.abc.Messageable.send:82 @@ -7674,31 +8158,31 @@ msgstr "メッセージのプッシュ通知とデスクトップ通知を抑制 msgid "Sending the message failed." msgstr "メッセージの送信に失敗した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:85 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:89 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:64 msgid "This webhook was not found." msgstr "Webhookが見つからなかった場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:86 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:90 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:65 msgid "The authorization token for the webhook is incorrect." msgstr "Webhookの認証トークンが正しくない場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:87 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:91 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:66 msgid "You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` or ``thread`` and ``thread_name``." msgstr "``embed`` と ``embeds`` または ``file`` と ``files`` または ``thread`` と ``thread_name`` の両方を指定した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:88 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:92 msgid "The length of ``embeds`` was invalid, there was no token associated with this webhook or ``ephemeral`` was passed with the improper webhook type or there was no state attached with this webhook when giving it a view." msgstr "``embeds`` の長さが不正な場合、Webhookにトークンが紐づいていない場合、 ``ephemeral`` が適切でない種類のWebhookに渡された場合、またはステートが付属していないWebhookにビューを渡した場合。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:90 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:94 #: ../../../discord/webhook/sync.py:docstring of discord.webhook.sync.SyncWebhook.send:69 msgid "If ``wait`` is ``True`` then the message that was sent, otherwise ``None``." msgstr "``wait`` が ``True`` のとき、送信されたメッセージ、それ以外の場合は ``None`` 。" -#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:91 +#: ../../../discord/webhook/async_.py:docstring of discord.webhook.async_.Webhook.send:95 msgid "Optional[:class:`WebhookMessage`]" msgstr "Optional[:class:`WebhookMessage`]" @@ -7899,7 +8383,7 @@ msgstr "メッセージの削除に失敗した場合。" msgid "Deleted a message that is not yours." msgstr "自分以外のメッセージを削除しようとした場合。" -#: ../../api.rst:4062 +#: ../../api.rst:4277 msgid "WebhookMessage" msgstr "WebhookMessage" @@ -8229,7 +8713,9 @@ msgid "The created thread." msgstr "作成されたスレッド。" #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.create_thread:31 +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:17 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.create_thread:31 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:17 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.create_thread:31 msgid ":class:`.Thread`" msgstr ":class:`.Thread`" @@ -8270,6 +8756,42 @@ msgstr "完全なメッセージ。" msgid ":class:`Message`" msgstr ":class:`Message`" +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:3 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:3 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:3 +msgid "Retrieves the public thread attached to this message." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:7 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:7 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:7 +msgid "This method is an API call. For general usage, consider :attr:`thread` instead." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:11 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:11 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:11 +msgid "An unknown channel type was received from Discord or the guild the thread belongs to is not the same as the one in this object points to." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:12 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:12 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:12 +msgid "Retrieving the thread failed." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:13 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:13 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:13 +msgid "There is no thread attached to this message." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.fetch_thread:16 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:16 +#: ../../../discord/message.py:docstring of discord.message.PartialMessage.fetch_thread:16 +msgid "The public thread attached to this message." +msgstr "" + #: ../../../discord/webhook/async_.py:docstring of discord.message.Message.is_system:1 #: ../../../discord/message.py:docstring of discord.message.Message.is_system:1 msgid ":class:`bool`: Whether the message is a system message." @@ -8492,6 +9014,25 @@ msgstr ":attr:`Message.type` に関わらず、レンダリングされた際の msgid "In the case of :attr:`MessageType.default` and :attr:`MessageType.reply`\\, this just returns the regular :attr:`Message.content`. Otherwise this returns an English message denoting the contents of the system message." msgstr ":attr:`MessageType.default` と :attr:`MessageType.reply` の場合、これは :attr:`Message.content` と同じものを返すだけです。しかしそれ以外の場合は、システムメッセージの英語版を返します。" +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:1 +#: ../../../discord/message.py:docstring of discord.Message.thread:1 +#: ../../../discord/message.py:docstring of discord.PartialMessage.thread:1 +msgid "The public thread created from this message, if it exists." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:5 +#: ../../../discord/message.py:docstring of discord.Message.thread:5 +msgid "For messages received via the gateway this does not retrieve archived threads, as they are not retained in the internal cache. Use :meth:`fetch_thread` instead." +msgstr "" + +#: ../../../discord/webhook/async_.py:docstring of discord.WebhookMessage.thread:10 +#: ../../../discord/message.py:docstring of discord.Message.thread:10 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.get_thread:14 +#: ../../../discord/channel.py:docstring of discord.channel.TextChannel.get_thread:14 +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.get_thread:14 +msgid "Optional[:class:`Thread`]" +msgstr "Optional[:class:`Thread`]" + #: ../../../discord/webhook/async_.py:docstring of discord.message.PartialMessage.to_reference:1 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.to_reference:1 #: ../../../discord/message.py:docstring of discord.message.PartialMessage.to_reference:1 @@ -8540,7 +9081,7 @@ msgstr "このメッセージのピン留めを外す権限を持っていない msgid "Unpinning the message failed." msgstr "メッセージのピン留め解除に失敗した場合。" -#: ../../api.rst:4071 +#: ../../api.rst:4286 msgid "SyncWebhook" msgstr "SyncWebhook" @@ -8593,7 +9134,7 @@ msgstr "このwebhookが送信した :class:`~discord.SyncWebhookMessage` を1 msgid ":class:`~discord.SyncWebhookMessage`" msgstr ":class:`~discord.SyncWebhookMessage`" -#: ../../api.rst:4080 +#: ../../api.rst:4295 msgid "SyncWebhookMessage" msgstr "SyncWebhookMessage" @@ -8607,19 +9148,19 @@ msgstr ":class:`SyncWebhookMessage`" msgid "If provided, the number of seconds to wait before deleting the message. This blocks the thread." msgstr "指定された場合、メッセージを削除するまでの待機秒数。これはスレッドをブロックします。" -#: ../../api.rst:4090 +#: ../../api.rst:4305 msgid "Abstract Base Classes" msgstr "抽象基底クラス" -#: ../../api.rst:4092 +#: ../../api.rst:4307 msgid "An :term:`abstract base class` (also known as an ``abc``) is a class that models can inherit to get their behaviour. **Abstract base classes should not be instantiated**. They are mainly there for usage with :func:`isinstance` and :func:`issubclass`\\." msgstr ":term:`abstract base class` ( ``abc`` という名称でも知られています)はモデルが振る舞いを得るために継承するクラスです。 **抽象基底クラスはインスタンス化されるべきではありません。** これらは主に :func:`isinstance` や :func:`issubclass` で使用するために存在します。" -#: ../../api.rst:4096 +#: ../../api.rst:4311 msgid "This library has a module related to abstract base classes, in which all the ABCs are subclasses of :class:`typing.Protocol`." msgstr "このライブラリには抽象基底クラスに関連するモジュールがあり、その中の抽象基底クラスは全て :class:`typing.Protocol` のサブクラスです。" -#: ../../api.rst:4100 +#: ../../api.rst:4315 msgid "Snowflake" msgstr "Snowflake" @@ -8639,8 +9180,8 @@ msgstr "スノーフレークを自分で作成したい場合は、 :class:`.Ob msgid "The model's unique ID." msgstr "モデルのユニークなID。" -#: ../../api.rst:4108 -#: ../../api.rst:4188 +#: ../../api.rst:4323 +#: ../../api.rst:4403 msgid "User" msgstr "User" @@ -8700,15 +9241,24 @@ msgid "Returns an Asset that represents the user's avatar, if present." msgstr "ユーザーのアバターが存在する場合は、それを表すアセットを返します。" #: ../../../discord/abc.py:docstring of discord.abc.User.avatar:3 +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration:5 msgid "Optional[:class:`~discord.Asset`]" msgstr "Optional[:class:`~discord.Asset`]" +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration:1 +msgid "Returns an Asset that represents the user's avatar decoration, if present." +msgstr "" + +#: ../../../discord/abc.py:docstring of discord.abc.User.avatar_decoration_sku_id:1 +msgid "Returns an integer that represents the user's avatar decoration SKU ID, if present." +msgstr "" + #: ../../../discord/abc.py:docstring of discord.abc.User.default_avatar:3 #: ../../../discord/abc.py:docstring of discord.abc.User.display_avatar:7 msgid ":class:`~discord.Asset`" msgstr ":class:`~discord.Asset`" -#: ../../api.rst:4116 +#: ../../api.rst:4331 msgid "PrivateChannel" msgstr "PrivateChannel" @@ -8732,7 +9282,7 @@ msgstr ":class:`~discord.GroupChannel`" msgid "The user presenting yourself." msgstr "あなた自身を示すユーザー。" -#: ../../api.rst:4124 +#: ../../api.rst:4339 msgid "GuildChannel" msgstr "GuildChannel" @@ -9600,7 +10150,7 @@ msgstr "現時点でアクティブな招待のリスト。" msgid "List[:class:`~discord.Invite`]" msgstr "List[:class:`~discord.Invite`]" -#: ../../api.rst:4132 +#: ../../api.rst:4347 msgid "Messageable" msgstr "Messageable" @@ -9944,7 +10494,7 @@ msgstr "メッセージ履歴の取得に失敗した場合。" msgid ":class:`~discord.Message` -- The message with the message data parsed." msgstr ":class:`~discord.Message` -- メッセージデータをパースしたメッセージ。" -#: ../../api.rst:4144 +#: ../../api.rst:4359 msgid "Connectable" msgstr "Connectable" @@ -9967,8 +10517,8 @@ msgstr ":attr:`~discord.Intents.voice_states` が必要です。" #: ../../../discord/abc.py:docstring of discord.abc.Connectable.connect:8 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:8 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:8 -msgid "The timeout in seconds to wait for the voice endpoint." -msgstr "ボイスエンドポイントを待つためのタイムアウト秒数。" +msgid "The timeout in seconds to wait the connection to complete." +msgstr "" #: ../../../discord/abc.py:docstring of discord.abc.Connectable.connect:10 #: ../../../discord/channel.py:docstring of discord.abc.Connectable.connect:10 @@ -10012,32 +10562,32 @@ msgstr "ボイスサーバーに完全に接続されたボイスクライアン msgid ":class:`~discord.VoiceProtocol`" msgstr ":class:`~discord.VoiceProtocol`" -#: ../../api.rst:4154 +#: ../../api.rst:4369 msgid "Discord Models" msgstr "Discordモデル" -#: ../../api.rst:4156 +#: ../../api.rst:4371 msgid "Models are classes that are received from Discord and are not meant to be created by the user of the library." msgstr "モデルはDiscordから受け取るクラスであり、ユーザーによって作成されることを想定していません。" -#: ../../api.rst:4161 +#: ../../api.rst:4376 msgid "The classes listed below are **not intended to be created by users** and are also **read-only**." msgstr "下記のクラスは、 **ユーザーによって作成されることを想定しておらず** 、中には **読み取り専用** のものもあります。" -#: ../../api.rst:4164 +#: ../../api.rst:4379 msgid "For example, this means that you should not make your own :class:`User` instances nor should you modify the :class:`User` instance yourself." msgstr "つまり、独自の :class:`User` を作成したりするべきではなく、また、 :class:`User` インスタンスの値の変更もするべきではありません。" -#: ../../api.rst:4167 +#: ../../api.rst:4382 msgid "If you want to get one of these model classes instances they'd have to be through the cache, and a common way of doing so is through the :func:`utils.find` function or attributes of model classes that you receive from the events specified in the :ref:`discord-api-events`." msgstr "このようなモデルクラスのインスタンスを取得したい場合は、 キャッシュを経由して取得する必要があります。一般的な方法としては :func:`utils.find` 関数を用いるか、 :ref:`discord-api-events` の特定のイベントから受け取る方法が挙げられます。" -#: ../../api.rst:4174 -#: ../../api.rst:4750 +#: ../../api.rst:4389 +#: ../../api.rst:5000 msgid "Nearly all classes here have :ref:`py:slots` defined which means that it is impossible to have dynamic attributes to the data classes." msgstr "ほぼすべてのクラスに :ref:`py:slots` が定義されています。つまり、データクラスに動的に変数を追加することは不可能です。" -#: ../../api.rst:4179 +#: ../../api.rst:4394 msgid "ClientUser" msgstr "ClientUser" @@ -10100,31 +10650,31 @@ msgstr "現在のクライアントのプロフィールを編集します。" msgid "To upload an avatar, a :term:`py:bytes-like object` must be passed in that represents the image being uploaded. If this is done through a file then the file must be opened via ``open('some_filename', 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``." msgstr "アバターをアップロードする際には、アップロードする画像を表す :term:`py:bytes-like object` を渡す必要があります。これをファイルを介して行う場合、ファイルを ``open('some_filename', 'rb')`` で開き、 :term:`py:bytes-like object` は ``fp.read()`` で取得できます。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:12 -msgid "The only image formats supported for uploading is JPEG and PNG." -msgstr "アップロードでサポートされる画像形式はJPEGとPNGのみです。" - -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:14 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:13 msgid "The edit is no longer in-place, instead the newly edited client user is returned." msgstr "編集はクライアントユーザーを置き換えず、編集された新しいクライアントユーザーが返されるようになりました。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:21 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:20 msgid "The new username you wish to change to." msgstr "変更する際の新しいユーザー名。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:23 -msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar." -msgstr "アップロードする画像を表す :term:`py:bytes-like object` 。アバターをなしにしたい場合は ``None`` を設定することが出来ます。" +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:22 +msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP." +msgstr "" + +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:26 +msgid "A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no banner. Only image formats supported for uploading are JPEG, PNG, GIF and WEBP." +msgstr "" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:27 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:33 msgid "Editing your profile failed." msgstr "プロフィールの編集に失敗した場合。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:28 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:34 msgid "Wrong image format passed for ``avatar``." msgstr "``avatar`` に渡された画像のフォーマットが間違っている場合。" -#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:30 +#: ../../../discord/user.py:docstring of discord.user.ClientUser.edit:36 msgid "The newly edited client user." msgstr "新しく編集されたクライアントユーザー。" @@ -10206,6 +10756,10 @@ msgstr "Set[:class:`int`]" msgid "The IDs of the channels that are exempt from the rule." msgstr "このルールの除外対象のチャンネルのID。" +#: ../../../discord/automod.py:docstring of discord.automod.AutoModRule:55 +msgid "The type of event that will trigger the the rule." +msgstr "" + #: ../../../discord/automod.py:docstring of discord.AutoModRule.creator:1 msgid "The member that created this rule." msgstr "このルールを作成したメンバー。" @@ -10429,7 +10983,7 @@ msgstr "ルールの取得に失敗した場合。" msgid "The rule that was executed." msgstr "発動されたルール。" -#: ../../api.rst:4214 +#: ../../api.rst:4429 msgid "Attachment" msgstr "Attachment" @@ -10501,10 +11055,6 @@ msgstr "添付ファイルが一時的であるかどうか。" msgid "The duration of the audio file in seconds. Returns ``None`` if it's not a voice message." msgstr "音声ファイルの秒単位の長さ。ボイスメッセージでない場合 ``None`` を返します。" -#: ../../../discord/message.py:docstring of discord.message.Attachment:99 -msgid "Optional[:class:`float`]" -msgstr "Optional[:class:`float`]" - #: ../../../discord/message.py:docstring of discord.message.Attachment:103 msgid "The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message." msgstr "音声の波形(振幅)をバイト単位で返します。ボイスメッセージでない場合は ``None`` を返します。" @@ -10513,6 +11063,14 @@ msgstr "音声の波形(振幅)をバイト単位で返します。ボイス msgid "Optional[:class:`bytes`]" msgstr "Optional[:class:`bytes`]" +#: ../../../discord/message.py:docstring of discord.Attachment.flags:1 +msgid "The attachment's flags." +msgstr "" + +#: ../../../discord/message.py:docstring of discord.Attachment.flags:3 +msgid ":class:`AttachmentFlags`" +msgstr "" + #: ../../../discord/message.py:docstring of discord.message.Attachment.is_spoiler:1 msgid ":class:`bool`: Whether this attachment contains a spoiler." msgstr ":class:`bool`: この添付ファイルにスポイラーが含まれているかどうか。" @@ -10607,7 +11165,7 @@ msgstr "送信に適したファイルに変換された添付ファイル。" msgid ":class:`File`" msgstr ":class:`File`" -#: ../../api.rst:4222 +#: ../../api.rst:4437 msgid "Asset" msgstr "Asset" @@ -10805,7 +11363,7 @@ msgstr "アセットがロッティータイプのスタンプであった場合 msgid "The asset as a file suitable for sending." msgstr "送信に適したファイルとしてのアセット。" -#: ../../api.rst:4231 +#: ../../api.rst:4446 msgid "Message" msgstr "Message" @@ -11077,7 +11635,7 @@ msgstr "もし指定したなら、これはメッセージを編集したあと msgid "Tried to suppress a message without permissions or edited a message's content or embed that isn't yours." msgstr "権限なしに埋め込みを除去しようとした場合や、他人のメッセージの内容や埋め込みを編集しようとした場合。" -#: ../../api.rst:4240 +#: ../../api.rst:4455 msgid "DeletedReferencedMessage" msgstr "DeletedReferencedMessage" @@ -11101,7 +11659,7 @@ msgstr "削除された参照されたメッセージが属していたチャン msgid "The guild ID of the deleted referenced message." msgstr "削除された参照されたメッセージが属していたギルドのID。" -#: ../../api.rst:4249 +#: ../../api.rst:4464 msgid "Reaction" msgstr "Reaction" @@ -11140,8 +11698,8 @@ msgid "Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]" msgstr "Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]" #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:34 -msgid "Number of times this reaction was made" -msgstr "リアクションが付けられた回数。" +msgid "Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`." +msgstr "" #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:40 msgid "If the user sent this reaction." @@ -11151,6 +11709,18 @@ msgstr "ボットがこのリアクションを付けたか。" msgid "Message this reaction is for." msgstr "このリアクションのメッセージ。" +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:52 +msgid "If the user sent this super reaction." +msgstr "" + +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:60 +msgid "The number of times this reaction was made using normal reactions. This is not available in the gateway events such as :func:`on_reaction_add` or :func:`on_reaction_remove`." +msgstr "" + +#: ../../../discord/reaction.py:docstring of discord.reaction.Reaction:70 +msgid "The number of times this reaction was made using super reactions. This is not available in the gateway events such as :func:`on_reaction_add` or :func:`on_reaction_remove`." +msgstr "" + #: ../../../discord/reaction.py:docstring of discord.reaction.Reaction.is_custom_emoji:1 msgid ":class:`bool`: If this is a custom emoji." msgstr ":class:`bool`: カスタム絵文字が使用されているかどうか。" @@ -11207,7 +11777,7 @@ msgstr "リアクションを付けたユーザーの取得に失敗した場合 msgid "Union[:class:`User`, :class:`Member`] -- The member (if retrievable) or the user that has reacted to this message. The case where it can be a :class:`Member` is in a guild message context. Sometimes it can be a :class:`User` if the member has left the guild." msgstr "Union[:class:`User`, :class:`Member`] -- リアクションを付けたメンバー(取得できる場合)かユーザー。ギルド内では :class:`Member` となります。メンバーがギルドを脱退した場合は :class:`User` になります。" -#: ../../api.rst:4257 +#: ../../api.rst:4472 msgid "Guild" msgstr "Guild" @@ -11456,6 +12026,7 @@ msgid "A list of forum channels that belongs to this guild." msgstr "このギルド内に存在するフォーラムチャンネルのリスト。" #: ../../../discord/guild.py:docstring of discord.Guild.forums:5 +#: ../../../discord/channel.py:docstring of discord.CategoryChannel.forums:5 msgid "List[:class:`ForumChannel`]" msgstr "List[:class:`ForumChannel`]" @@ -11515,12 +12086,6 @@ msgstr "内部キャッシュに保持されていないため、アーカイブ msgid "The returned thread or ``None`` if not found." msgstr "スレッド、または該当するものが見つからない場合 ``None`` が返ります。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.get_thread:14 -#: ../../../discord/channel.py:docstring of discord.channel.TextChannel.get_thread:14 -#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.get_thread:14 -msgid "Optional[:class:`Thread`]" -msgstr "Optional[:class:`Thread`]" - #: ../../../discord/guild.py:docstring of discord.guild.Guild.get_emoji:8 msgid "The returned Emoji or ``None`` if not found." msgstr "絵文字、または該当するものが見つからない場合 ``None`` が返ります。" @@ -12181,24 +12746,32 @@ msgstr "レイドプロテクションのアラートをギルドで無効にす msgid "The new channel that is used for safety alerts. This is only available to guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no safety alerts channel." msgstr "安全アラートに使用される新しいチャンネル。これは :attr:`Guild.features` に ``COMMUNITY`` を含むギルドでのみ利用できます。安全アラートチャネルがない場合は ``None`` を指定できます。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:122 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:121 +msgid "The time when invites should be enabled again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:126 +msgid "The time when direct messages should be allowed again, or ``None`` to disable the action. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:132 msgid "You do not have permissions to edit the guild." msgstr "ギルドを編集する権限がない場合。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:123 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:133 #: ../../../discord/integrations.py:docstring of discord.integrations.StreamIntegration.edit:19 msgid "Editing the guild failed." msgstr "ギルドの編集に失敗した場合。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:124 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:134 msgid "The image format passed in to ``icon`` is invalid. It must be PNG or JPG. This is also raised if you are not the owner of the guild and request an ownership transfer." msgstr "``icon`` に渡された画像形式が無効な場合。これはPNGかJPGでなくてはいけません。また、あなたがギルドの所有者でないのに、ギルドの所有権の移動を行おうとした場合にも発生します。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:125 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:135 msgid "The type passed to the ``default_notifications``, ``rules_channel``, ``public_updates_channel``, ``safety_alerts_channel`` ``verification_level``, ``explicit_content_filter``, ``system_channel_flags``, or ``mfa_level`` parameter was of the incorrect type." msgstr "``default_notifications`` 、 ``rules_channel`` 、 ``public_updates_channel`` 、 ``safety_alerts_channel`` 、 ``verification_level`` 、 ``explicit_content_filter`` 、 ``system_channel_flags`` 、 ``mfa_level`` に間違った型の値が渡されたとき。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:127 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit:137 msgid "The newly updated guild. Note that this has the same limitations as mentioned in :meth:`Client.fetch_guild` and may not have full data." msgstr "新しく更新されたギルド。これは :meth:`Client.fetch_guild` に記載されているものと同じ制限があり、完全なデータを持っていない可能性があることに注意してください。" @@ -12285,6 +12858,10 @@ msgstr "``member_id`` 引数は位置専用引数となりました。" msgid "The member's ID to fetch from." msgstr "取得したいメンバーのID。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:16 +msgid "You do not have access to the guild." +msgstr "ギルドにアクセスする権限がない場合。" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.fetch_member:17 msgid "Fetching the member failed." msgstr "メンバーの取得に失敗した場合。" @@ -12358,10 +12935,6 @@ msgstr "このユーザ以前のBANを取得します。" msgid "Retrieve bans after this user." msgstr "このユーザ以降のBANを取得します。" -#: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:33 -msgid "Both ``after`` and ``before`` were provided, as Discord does not support this type of pagination." -msgstr "``after`` と ``before`` の両方が渡された場合。Discordはこのタイプのページネーションをサポートしていません。" - #: ../../../discord/guild.py:docstring of discord.guild.Guild.bans:35 msgid ":class:`BanEntry` -- The ban entry of the banned user." msgstr ":class:`BanEntry` -- BANされたユーザーのBANエントリー。" @@ -12375,9 +12948,8 @@ msgid "The inactive members are denoted if they have not logged on in ``days`` n msgstr "``days`` 日間ログインせずロールを持たないメンバーが非アクティブとされます。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.prune_members:8 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:7 -msgid "You must have :attr:`~Permissions.kick_members` to do this." -msgstr "これを行うには、 :attr:`~Permissions.kick_members` が必要です。" +msgid "You must have both :attr:`~Permissions.kick_members` and :attr:`~Permissions.manage_guild` to do this." +msgstr "" #: ../../../discord/guild.py:docstring of discord.guild.Guild.prune_members:10 msgid "To check how many members you would prune without actually pruning, see the :meth:`estimate_pruned_members` function." @@ -13086,7 +13658,6 @@ msgid "The guild must have ``COMMUNITY`` in :attr:`~Guild.features`." msgstr ":attr:`~Guild.features` に ``COMMUNITY`` が含まれている必要があります。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.welcome_screen:7 -#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:8 #: ../../../discord/guild.py:docstring of discord.guild.Guild.vanity_invite:7 msgid "You must have :attr:`~Permissions.manage_guild` to do this.as well." msgstr "これを行うには、 :attr:`~Permissions.manage_guild` も必要です。" @@ -13113,6 +13684,10 @@ msgstr ":class:`WelcomeScreen`" msgid "A shorthand method of :attr:`WelcomeScreen.edit` without needing to fetch the welcome screen beforehand." msgstr "ようこそ画面を事前に取得せずに :attr:`WelcomeScreen.edit` を呼び出すことのできるメソッド。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:8 +msgid "You must have :attr:`~Permissions.manage_guild` to do this as well." +msgstr "" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.edit_welcome_screen:12 msgid "The edited welcome screen." msgstr "編集した後のようこそ画面。" @@ -13127,6 +13702,10 @@ msgstr "サーバーからユーザーをキックします。" msgid "The user must meet the :class:`abc.Snowflake` abc." msgstr "ユーザーは :class:`abc.Snowflake` 抽象基底クラスのサブクラスである必要があります。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:7 +msgid "You must have :attr:`~Permissions.kick_members` to do this." +msgstr "これを行うには、 :attr:`~Permissions.kick_members` が必要です。" + #: ../../../discord/guild.py:docstring of discord.guild.Guild.kick:9 msgid "The user to kick from their guild." msgstr "ギルドからキックするユーザー。" @@ -13149,10 +13728,12 @@ msgstr "ギルドからユーザーをBANします。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:7 #: ../../../discord/guild.py:docstring of discord.guild.Guild.unban:7 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:7 msgid "You must have :attr:`~Permissions.ban_members` to do this." msgstr "これを行うには、 :attr:`~Permissions.ban_members` が必要です。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:9 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:11 msgid "The user to ban from their guild." msgstr "ギルドからBANするユーザー。" @@ -13173,10 +13754,12 @@ msgid "The requested user was not found." msgstr "ユーザーが見つからなかった場合。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:29 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:20 msgid "You do not have the proper permissions to ban." msgstr "BANするのに適切な権限がない場合。" #: ../../../discord/guild.py:docstring of discord.guild.Guild.ban:30 +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:21 msgid "Banning failed." msgstr "BANに失敗した場合。" @@ -13204,6 +13787,30 @@ msgstr "BANを解除するのに適切な権限がない場合。" msgid "Unbanning failed." msgstr "BAN解除に失敗した場合。" +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:3 +msgid "Bans multiple users from the guild." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:5 +msgid "The users must meet the :class:`abc.Snowflake` abc." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:13 +msgid "The number of seconds worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 604800 (7 days). Defaults to 1 day." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:17 +msgid "The reason the users got banned." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:23 +msgid "The result of the bulk ban operation." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.bulk_ban:24 +msgid ":class:`BulkBanResult`" +msgstr "" + #: ../../../discord/guild.py:docstring of discord.Guild.vanity_url:1 msgid "The Discord vanity invite URL for this guild, if available." msgstr "存在する場合、ギルドのDiscord バニティURLを返します。" @@ -13454,19 +14061,52 @@ msgstr "自動管理ルールの作成に失敗した場合。" msgid "The automod rule that was created." msgstr "作成された自動管理ルール。" -#: ../../api.rst:4266 +#: ../../../discord/guild.py:docstring of discord.Guild.invites_paused_until:1 +msgid "If invites are paused, returns when invites will get enabled in UTC, otherwise returns None." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.Guild.dms_paused_until:1 +msgid "If DMs are paused, returns when DMs will get enabled in UTC, otherwise returns None." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.invites_paused:1 +msgid ":class:`bool`: Whether invites are paused in the guild." +msgstr "" + +#: ../../../discord/guild.py:docstring of discord.guild.Guild.dms_paused:1 +msgid ":class:`bool`: Whether DMs are paused in the guild." +msgstr "" + +#: ../../api.rst:4481 msgid "A namedtuple which represents a ban returned from :meth:`~Guild.bans`." msgstr ":meth:`~Guild.bans` から返されたBANを表すnamedtuple。" -#: ../../api.rst:4270 +#: ../../api.rst:4485 msgid "The reason this user was banned." msgstr "ユーザーがBANされた理由。" -#: ../../api.rst:4275 +#: ../../api.rst:4490 msgid "The :class:`User` that was banned." msgstr "BANされた :class:`User` 。" -#: ../../api.rst:4281 +#: ../../api.rst:4496 +msgid "A namedtuple which represents the result returned from :meth:`~Guild.bulk_ban`." +msgstr "" + +#: ../../api.rst:4502 +msgid "The list of users that were banned. The inner :class:`Object` of the list has the :attr:`Object.type` set to :class:`User`." +msgstr "" + +#: ../../api.rst:4505 +#: ../../api.rst:4511 +msgid "List[:class:`Object`]" +msgstr "" + +#: ../../api.rst:4508 +msgid "The list of users that could not be banned. The inner :class:`Object` of the list has the :attr:`Object.type` set to :class:`User`." +msgstr "" + +#: ../../api.rst:4515 msgid "ScheduledEvent" msgstr "ScheduledEvent" @@ -13719,7 +14359,7 @@ msgstr "このイベントに購読済みのユーザー。" msgid "List[:class:`User`]" msgstr "List[:class:`User`]" -#: ../../api.rst:4290 +#: ../../api.rst:4524 msgid "Integration" msgstr "Integration" @@ -13933,7 +14573,7 @@ msgstr "不完全なギルドの連携サービス。" msgid "The id of the application this integration belongs to." msgstr "このインテグレーションが属するアプリケーションのID。" -#: ../../api.rst:4323 +#: ../../api.rst:4557 msgid "Member" msgstr "Member" @@ -14057,6 +14697,14 @@ msgstr ":attr:`User.accent_color` と同じです。" msgid "Equivalent to :attr:`User.accent_colour`" msgstr ":attr:`User.accent_colour` と同じです。" +#: ../../../discord/member.py:docstring of discord.Member.avatar_decoration:1 +msgid "Equivalent to :attr:`User.avatar_decoration`" +msgstr "" + +#: ../../../discord/member.py:docstring of discord.Member.avatar_decoration_sku_id:1 +msgid "Equivalent to :attr:`User.avatar_decoration_sku_id`" +msgstr "" + #: ../../../discord/member.py:docstring of discord.Member.raw_status:1 msgid "The member's overall status as a string value." msgstr "メンバーのステータスを文字列として返します。" @@ -14329,8 +14977,8 @@ msgstr "メンバーを編集する理由。監査ログに表示されます。 #: ../../../discord/member.py:docstring of discord.member.Member.edit:60 #: ../../../discord/member.py:docstring of discord.member.Member.request_to_speak:15 -msgid "You do not have the proper permissions to the action requested." -msgstr "リクエストされたアクションをするための適切な権限がない場合。" +msgid "You do not have the proper permissions to do the action requested." +msgstr "" #: ../../../discord/member.py:docstring of discord.member.Member.edit:61 #: ../../../discord/member.py:docstring of discord.member.Member.request_to_speak:16 @@ -14475,7 +15123,7 @@ msgstr "このメンバーがタイムアウトされているかどうかを返 msgid "``True`` if the member is timed out. ``False`` otherwise." msgstr "メンバーがタイムアウトした場合は ``True`` 。そうでなければ、 ``False`` 。" -#: ../../api.rst:4336 +#: ../../api.rst:4570 msgid "Spotify" msgstr "Spotify" @@ -14592,7 +15240,7 @@ msgstr ":class:`datetime.timedelta`" msgid "The party ID of the listening party." msgstr "リスニングパーティーのパーティーID。" -#: ../../api.rst:4344 +#: ../../api.rst:4578 msgid "VoiceState" msgstr "VoiceState" @@ -14644,7 +15292,7 @@ msgstr "ユーザーがギルドのAFKチャンネルにいるかどうかを示 msgid "The voice channel that the user is currently connected to. ``None`` if the user is not currently in a voice channel." msgstr "ユーザーが現在接続しているボイスチャンネル。ユーザーがボイスチャンネルに接続していない場合は ``None`` 。" -#: ../../api.rst:4352 +#: ../../api.rst:4586 msgid "Emoji" msgstr "Emoji" @@ -14764,7 +15412,7 @@ msgstr "絵文字の編集中にエラーが発生した場合。" msgid "The newly updated emoji." msgstr "新しく更新された絵文字。" -#: ../../api.rst:4361 +#: ../../api.rst:4595 msgid "PartialEmoji" msgstr "PartialEmoji" @@ -14862,7 +15510,7 @@ msgstr "これがカスタム絵文字でない場合は、空の文字列が返 msgid "The PartialEmoji is not a custom emoji." msgstr "PartialEmojiがカスタム絵文字でない場合。" -#: ../../api.rst:4370 +#: ../../api.rst:4604 msgid "Role" msgstr "Role" @@ -15018,6 +15666,14 @@ msgstr "ロールをメンションすることのできる文字列を返しま msgid "Returns all the members with this role." msgstr "このロールを持つすべてのメンバーを返します。" +#: ../../../discord/role.py:docstring of discord.Role.flags:1 +msgid "Returns the role's flags." +msgstr "" + +#: ../../../discord/role.py:docstring of discord.Role.flags:5 +msgid ":class:`RoleFlags`" +msgstr "" + #: ../../../discord/role.py:docstring of discord.role.Role.edit:3 msgid "Edits the role." msgstr "ロールを編集します。" @@ -15090,7 +15746,7 @@ msgstr "ロールを削除する権限がない場合。" msgid "Deleting the role failed." msgstr "ロールの削除に失敗した場合。" -#: ../../api.rst:4378 +#: ../../api.rst:4612 msgid "RoleTags" msgstr "RoleTags" @@ -15126,7 +15782,7 @@ msgstr ":class:`bool`: ロールが購入可能かどうか。" msgid ":class:`bool`: Whether the role is a guild's linked role." msgstr ":class:`bool`: ロールがギルドの関連付けられたロールかどうか。" -#: ../../api.rst:4386 +#: ../../api.rst:4620 msgid "PartialMessageable" msgstr "PartialMessageable" @@ -15241,7 +15897,7 @@ msgstr "部分的なメッセージ。" msgid ":class:`PartialMessage`" msgstr ":class:`PartialMessage`" -#: ../../api.rst:4395 +#: ../../api.rst:4629 msgid "TextChannel" msgstr "TextChannel" @@ -15490,13 +16146,13 @@ msgid "The new ``position`` is less than 0 or greater than the number of channel msgstr "新しい ``position`` が0より小さいか、カテゴリの数より大きい場合。" #: ../../../discord/channel.py:docstring of discord.channel.TextChannel.edit:58 -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:55 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:60 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:51 msgid "You do not have permissions to edit the channel." msgstr "チャンネルを編集する権限がない場合。" #: ../../../discord/channel.py:docstring of discord.channel.TextChannel.edit:59 -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:56 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:61 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:52 msgid "Editing the channel failed." msgstr "チャンネルの編集に失敗した場合。" @@ -15874,7 +16530,7 @@ msgstr "``joined`` が ``True`` に設定され、``private`` が ``False`` に msgid ":class:`Thread` -- The archived threads." msgstr ":class:`Thread` -- アーカイブされたスレッド。" -#: ../../api.rst:4408 +#: ../../api.rst:4642 msgid "ForumChannel" msgstr "ForumChannel" @@ -15934,13 +16590,6 @@ msgstr "フォーラムに年齢制限がかかっているか。" msgid "The default auto archive duration in minutes for threads created in this forum." msgstr "このフォーラムで作成されたスレッドのデフォルトの分単位の自動アーカイブ期間。" -#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:105 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:92 -#: ../../../discord/activity.py:docstring of discord.activity.CustomActivity:33 -#: ../../../discord/channel.py:docstring of discord.channel.ForumTag:48 -msgid "Optional[:class:`PartialEmoji`]" -msgstr "Optional[:class:`PartialEmoji`]" - #: ../../../discord/channel.py:docstring of discord.channel.ForumChannel:109 msgid "The default layout for posts in this forum channel. Defaults to :attr:`ForumLayoutType.not_set`." msgstr "このフォーラムチャネルの投稿のデフォルトの表示レイアウト。デフォルトは :attr:`ForumLayoutType.not_set` です。" @@ -15978,6 +16627,10 @@ msgstr "Optional[:class:`ForumTag`]" msgid ":class:`bool`: Checks if the forum is NSFW." msgstr ":class:`bool`: フォーラムに年齢制限があるかどうかをチェックします。" +#: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.is_media:1 +msgid ":class:`bool`: Checks if the channel is a media channel." +msgstr "" + #: ../../../discord/channel.py:docstring of discord.channel.ForumChannel.edit:3 msgid "Edits the forum." msgstr "フォーラムを編集します。" @@ -16143,7 +16796,7 @@ msgstr "このフォーラムののアーカイブされたスレッドを :attr msgid "You must have :attr:`~Permissions.read_message_history` to do this." msgstr "これを行うためには、 :attr:`~Permissions.read_message_history` が必要です。" -#: ../../api.rst:4417 +#: ../../api.rst:4651 msgid "Thread" msgstr "Thread" @@ -16591,7 +17244,7 @@ msgstr "スレッドを削除する権限がない場合。" msgid "Deleting the thread failed." msgstr "スレッドの削除に失敗した場合。" -#: ../../api.rst:4430 +#: ../../api.rst:4664 msgid "ThreadMember" msgstr "ThreadMember" @@ -16631,7 +17284,7 @@ msgstr "メンバーがスレッドに参加したUTC時刻。" msgid "The thread this member belongs to." msgstr "メンバーが属するスレッド。" -#: ../../api.rst:4438 +#: ../../api.rst:4672 msgid "VoiceChannel" msgstr "VoiceChannel" @@ -16666,16 +17319,20 @@ msgstr "チャンネルの新しいユーザー人数制限。" msgid "The new region for the voice channel's voice communication. A value of ``None`` indicates automatic voice region detection." msgstr "ボイスチャンネルの新しい音声通信のためのリージョン。値が ``None`` の場合は自動で検出されます。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:54 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:53 +msgid "The new voice channel status. It can be up to 500 characters. Can be ``None`` to remove the status." +msgstr "" + +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:59 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.edit:50 msgid "If the permission overwrite information is not in proper form." msgstr "権限の上書きの情報が適切なものでない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:58 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:63 msgid "The newly edited voice channel. If the edit was only positional then ``None`` is returned instead." msgstr "新しく編集されたボイスチャンネル。編集が位置のみだった場合は代わりに ``None`` が返されます。" -#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:60 +#: ../../../discord/channel.py:docstring of discord.channel.VoiceChannel.edit:65 msgid "Optional[:class:`.VoiceChannel`]" msgstr "Optional[:class:`.VoiceChannel`]" @@ -16709,7 +17366,7 @@ msgstr "メンバーIDをキーとしボイス状態を値とするマッピン msgid "Mapping[:class:`int`, :class:`VoiceState`]" msgstr "Mapping[:class:`int`, :class:`VoiceState`]" -#: ../../api.rst:4447 +#: ../../api.rst:4681 msgid "StageChannel" msgstr "StageChannel" @@ -16771,27 +17428,31 @@ msgid "Whether to send a start notification. This sends a push notification to @ msgstr "開始通知を送信するかどうか。 ``True`` の場合、プッシュ通知を@everyoneに送信します。 デフォルトは ``False`` です。これを行うには、 :attr:`~Permissions.mention_everyone` が必要です。" #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:18 +msgid "The guild scheduled event associated with the stage instance." +msgstr "" + +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:22 msgid "The reason the stage instance was created. Shows up on the audit log." msgstr "ステージインスタンスを作成する理由。監査ログに表示されます。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:21 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:25 #: ../../../discord/stage_instance.py:docstring of discord.stage_instance.StageInstance.edit:14 msgid "If the ``privacy_level`` parameter is not the proper type." msgstr "引数 ``privacy_level`` が適切な型でない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:22 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:26 msgid "You do not have permissions to create a stage instance." msgstr "ステージインスタンスを作成する権限がない場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:23 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:27 msgid "Creating a stage instance failed." msgstr "ステージインスタンスの作成に失敗した場合。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:25 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:29 msgid "The newly created stage instance." msgstr "新しく作成されたステージインスタンス。" -#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:26 +#: ../../../discord/channel.py:docstring of discord.channel.StageChannel.create_instance:30 #: ../../../discord/channel.py:docstring of discord.channel.StageChannel.fetch_instance:11 msgid ":class:`StageInstance`" msgstr ":class:`StageInstance`" @@ -16820,7 +17481,7 @@ msgstr "新しく編集されたステージチャンネル。編集が位置の msgid "Optional[:class:`.StageChannel`]" msgstr "Optional[:class:`.StageChannel`]" -#: ../../api.rst:4457 +#: ../../api.rst:4691 msgid "StageInstance" msgstr "StageInstance" @@ -16920,7 +17581,7 @@ msgstr "ステージインスタンスを削除する権限がない場合。" msgid "Deleting the stage instance failed." msgstr "ステージインスタンスの削除に失敗した場合。" -#: ../../api.rst:4465 +#: ../../api.rst:4699 msgid "CategoryChannel" msgstr "CategoryChannel" @@ -17032,6 +17693,10 @@ msgstr "このカテゴリに属するボイスチャンネルを返します。 msgid "Returns the stage channels that are under this category." msgstr "このカテゴリに属するステージチャンネルを返します。" +#: ../../../discord/channel.py:docstring of discord.CategoryChannel.forums:1 +msgid "Returns the forum channels that are under this category." +msgstr "" + #: ../../../discord/channel.py:docstring of discord.channel.CategoryChannel.create_text_channel:3 msgid "A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category." msgstr "カテゴリ内に :class:`TextChannel` を作成するための :meth:`Guild.create_text_channel` のショートカット。" @@ -17048,7 +17713,7 @@ msgstr "カテゴリ内に :class:`StageChannel` を作成するための :meth: msgid "A shortcut method to :meth:`Guild.create_forum` to create a :class:`ForumChannel` in the category." msgstr "カテゴリ内に :class:`ForumChannel` を作成するための :meth:`Guild.create_forum` のショートカット。" -#: ../../api.rst:4474 +#: ../../api.rst:4708 msgid "DMChannel" msgstr "DMChannel" @@ -17127,7 +17792,7 @@ msgstr ":attr:`~Permissions.send_messages_in_threads`: DMにはスレッドが msgid "Thread related permissions are now set to ``False``." msgstr "スレッド関連の権限が ``False`` に設定されるようになりました。" -#: ../../api.rst:4487 +#: ../../api.rst:4721 msgid "GroupChannel" msgstr "GroupChannel" @@ -17187,7 +17852,7 @@ msgstr "あなたがグループにいる唯一の者である場合、グルー msgid "Leaving the group failed." msgstr "グループの脱退に失敗した場合。" -#: ../../api.rst:4500 +#: ../../api.rst:4734 msgid "PartialInviteGuild" msgstr "PartialInviteGuild" @@ -17254,7 +17919,7 @@ msgstr "部分的なギルドの現在の「ブースト」数。" msgid "The Discord vanity invite URL for this partial guild, if available." msgstr "存在する場合、部分的なギルドのDiscord バニティURL。" -#: ../../api.rst:4508 +#: ../../api.rst:4742 msgid "PartialInviteChannel" msgstr "PartialInviteChannel" @@ -17296,7 +17961,7 @@ msgstr "部分的なチャンネルのID。" msgid "The partial channel's type." msgstr "部分的なチャンネルの種類。" -#: ../../api.rst:4516 +#: ../../api.rst:4750 msgid "Invite" msgstr "Invite" @@ -17501,7 +18166,7 @@ msgstr "インスタント招待を取り消します。" msgid "The reason for deleting this invite. Shows up on the audit log." msgstr "招待を削除する理由。監査ログに表示されます。" -#: ../../api.rst:4524 +#: ../../api.rst:4758 msgid "Template" msgstr "Template" @@ -17604,7 +18269,7 @@ msgstr "テンプレートを削除します。" msgid "The template url." msgstr "テンプレートのURL。" -#: ../../api.rst:4532 +#: ../../api.rst:4766 msgid "WelcomeScreen" msgstr "WelcomeScreen" @@ -17676,7 +18341,7 @@ msgstr "ようこそ画面を編集するのに必要な権限がない場合。 msgid "This welcome screen does not exist." msgstr "ようこそ画面が存在しない場合。" -#: ../../api.rst:4540 +#: ../../api.rst:4774 msgid "WelcomeChannel" msgstr "WelcomeChannel" @@ -17704,7 +18369,7 @@ msgstr "チャンネルの説明の横に使用される絵文字。" msgid "Optional[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]" msgstr "Optional[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]" -#: ../../api.rst:4548 +#: ../../api.rst:4782 msgid "WidgetChannel" msgstr "WidgetChannel" @@ -17720,7 +18385,7 @@ msgstr "チャンネルのID。" msgid "The channel's position" msgstr "チャンネルの位置。" -#: ../../api.rst:4556 +#: ../../api.rst:4790 msgid "WidgetMember" msgstr "WidgetMember" @@ -17804,7 +18469,7 @@ msgstr "Optional[:class:`WidgetChannel`]" msgid "Returns the member's display name." msgstr "メンバーの表示名を返します。" -#: ../../api.rst:4565 +#: ../../api.rst:4799 msgid "Widget" msgstr "Widget" @@ -17876,7 +18541,7 @@ msgstr "招待にカウント情報を含めるかどうか。これにより :a msgid "The invite from the widget's invite URL, if available." msgstr "利用可能な場合は、ウィジェットの招待URLからの招待。" -#: ../../api.rst:4573 +#: ../../api.rst:4807 msgid "StickerPack" msgstr "StickerPack" @@ -17936,7 +18601,7 @@ msgstr "Optional[:class:`StandardSticker`]" msgid "The banner asset of the sticker pack." msgstr "スタンプパックのバナーアセット。" -#: ../../api.rst:4581 +#: ../../api.rst:4815 msgid "StickerItem" msgstr "StickerItem" @@ -17990,7 +18655,7 @@ msgstr "スタンプアイテムの完全なスタンプデータを取得する msgid "Union[:class:`StandardSticker`, :class:`GuildSticker`]" msgstr "Union[:class:`StandardSticker`, :class:`GuildSticker`]" -#: ../../api.rst:4589 +#: ../../api.rst:4823 msgid "Sticker" msgstr "Sticker" @@ -18031,7 +18696,7 @@ msgstr "スタンプパックのID。" msgid "Returns the sticker's creation time in UTC." msgstr "スタンプの作成された時間をUTCで返します。" -#: ../../api.rst:4597 +#: ../../api.rst:4831 msgid "StandardSticker" msgstr "StandardSticker" @@ -18067,7 +18732,7 @@ msgstr "取得したスタンプパック。" msgid ":class:`StickerPack`" msgstr ":class:`StickerPack`" -#: ../../api.rst:4605 +#: ../../api.rst:4839 msgid "GuildSticker" msgstr "GuildSticker" @@ -18123,7 +18788,7 @@ msgstr "スタンプの編集中にエラーが発生した場合。" msgid "The newly modified sticker." msgstr "新しく変更されたスタンプ。" -#: ../../api.rst:4613 +#: ../../api.rst:4847 msgid "ShardInfo" msgstr "ShardInfo" @@ -18167,7 +18832,127 @@ msgstr "シャードを接続します。もしすでに接続されている場 msgid "Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard." msgstr "このシャードのHEARTBEATとHEARTBEAT_ACK間の待ち時間を秒単位で測定します。" -#: ../../api.rst:4621 +#: ../../api.rst:4855 +msgid "SKU" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:1 +msgid "Represents a premium offering as a stock-keeping unit (SKU)." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:7 +msgid "The SKU's ID." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:13 +msgid "The type of the SKU." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:15 +msgid ":class:`SKUType`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:19 +msgid "The ID of the application that the SKU belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:25 +msgid "The consumer-facing name of the premium offering." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.SKU:31 +msgid "A system-generated URL slug based on the SKU name." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.flags:1 +msgid "Returns the flags of the SKU." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.flags:3 +msgid ":class:`SKUFlags`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.SKU.created_at:1 +msgid "Returns the sku's creation time in UTC." +msgstr "" + +#: ../../api.rst:4863 +msgid "Entitlement" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:1 +msgid "Represents an entitlement from user or guild which has been granted access to a premium offering." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:7 +msgid "The entitlement's ID." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:13 +msgid "The ID of the SKU that the entitlement belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:19 +msgid "The ID of the application that the entitlement belongs to." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:25 +msgid "The ID of the user that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:31 +msgid "The type of the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:33 +msgid ":class:`EntitlementType`" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:37 +msgid "Whether the entitlement has been deleted." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:43 +msgid "A UTC start date which the entitlement is valid. Not present when using test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:49 +msgid "A UTC date which entitlement is no longer valid. Not present when using test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement:55 +msgid "The ID of the guild that is granted access to the entitlement" +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.user:1 +msgid "The user that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.guild:1 +msgid "The guild that is granted access to the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.Entitlement.created_at:1 +msgid "Returns the entitlement's creation time in UTC." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.is_expired:1 +msgid ":class:`bool`: Returns ``True`` if the entitlement is expired. Will be always False for test entitlements." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:3 +msgid "Deletes the entitlement." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:6 +msgid "The entitlement could not be found." +msgstr "" + +#: ../../../discord/sku.py:docstring of discord.sku.Entitlement.delete:7 +msgid "Deleting the entitlement failed." +msgstr "" + +#: ../../api.rst:4871 msgid "RawMessageDeleteEvent" msgstr "RawMessageDeleteEvent" @@ -18192,7 +18977,7 @@ msgstr "削除されたメッセージ ID。" msgid "The cached message, if found in the internal message cache." msgstr "内部のメッセージキャッシュに見つかった場合、そのキャッシュされたメッセージ。" -#: ../../api.rst:4629 +#: ../../api.rst:4879 msgid "RawBulkMessageDeleteEvent" msgstr "RawBulkMessageDeleteEvent" @@ -18220,7 +19005,7 @@ msgstr "内部のメッセージキャッシュに見つかった場合、その msgid "List[:class:`Message`]" msgstr "List[:class:`Message`]" -#: ../../api.rst:4637 +#: ../../api.rst:4887 msgid "RawMessageUpdateEvent" msgstr "RawMessageUpdateEvent" @@ -18247,8 +19032,8 @@ msgstr ":ddocs:`ゲートウェイ ` によって #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawMessageUpdateEvent:29 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawThreadUpdateEvent:33 #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawThreadMembersUpdate:27 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:57 -#: ../../../discord/activity.py:docstring of discord.activity.Activity:69 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:65 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:77 msgid ":class:`dict`" msgstr ":class:`dict`" @@ -18256,7 +19041,7 @@ msgstr ":class:`dict`" msgid "The cached message, if found in the internal message cache. Represents the message before it is modified by the data in :attr:`RawMessageUpdateEvent.data`." msgstr "内部メッセージキャッシュで見つかった場合、そのキャッシュされたメッセージ。 :attr:`RawMessageUpdateEvent.data` のデータによって変更される前のメッセージを表します。" -#: ../../api.rst:4645 +#: ../../api.rst:4895 msgid "RawReactionActionEvent" msgstr "RawReactionActionEvent" @@ -18289,10 +19074,30 @@ msgid "The member who added the reaction. Only available if ``event_type`` is `` msgstr "リアクションを追加したメンバー。 ``event_type`` が ``REACTION_ADD`` でリアクションがギルド内にある場合にのみ利用できます。" #: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:44 +msgid "The author ID of the message being reacted to. Only available if ``event_type`` is ``REACTION_ADD``." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:52 msgid "The event type that triggered this action. Can be ``REACTION_ADD`` for reaction addition or ``REACTION_REMOVE`` for reaction removal." msgstr "このアクションの原因であるイベントタイプ。リアクションの追加は ``REACTION_ADD`` 、リアクションの除去は ``REACTION_REMOVE`` です。" -#: ../../api.rst:4653 +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:62 +msgid "Whether the reaction was a burst reaction, also known as a \"super reaction\"." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:70 +msgid "A list of colours used for burst reaction animation. Only available if ``burst`` is ``True`` and if ``event_type`` is ``REACTION_ADD``." +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.raw_models.RawReactionActionEvent:75 +msgid "List[:class:`Colour`]" +msgstr "" + +#: ../../../discord/raw_models.py:docstring of discord.RawReactionActionEvent.burst_colors:1 +msgid "An alias of :attr:`burst_colours`." +msgstr "" + +#: ../../api.rst:4903 msgid "RawReactionClearEvent" msgstr "RawReactionClearEvent" @@ -18315,7 +19120,7 @@ msgstr "リアクションの一括除去が行われたチャンネルのID。" msgid "The guild ID where the reactions got cleared." msgstr "リアクションの一括除去が行われたギルドのID。" -#: ../../api.rst:4661 +#: ../../api.rst:4911 msgid "RawReactionClearEmojiEvent" msgstr "RawReactionClearEmojiEvent" @@ -18327,7 +19132,7 @@ msgstr ":func:`on_raw_reaction_clear_emoji` イベントのペイロードを表 msgid "The custom or unicode emoji being removed." msgstr "除去されたカスタムまたはユニコード絵文字。" -#: ../../api.rst:4669 +#: ../../api.rst:4919 msgid "RawIntegrationDeleteEvent" msgstr "RawIntegrationDeleteEvent" @@ -18347,7 +19152,7 @@ msgstr "削除された連携サービスのボットやOAuth2 アプリケー msgid "The guild ID where the integration got deleted." msgstr "連携サービスが削除されたギルドのID。" -#: ../../api.rst:4677 +#: ../../api.rst:4927 msgid "RawThreadUpdateEvent" msgstr "RawThreadUpdateEvent" @@ -18392,7 +19197,7 @@ msgstr "スレッドが内部キャッシュで見つかった場合、そのス msgid "Optional[:class:`discord.Thread`]" msgstr "Optional[:class:`discord.Thread`]" -#: ../../api.rst:4685 +#: ../../api.rst:4935 msgid "RawThreadMembersUpdate" msgstr "RawThreadMembersUpdate" @@ -18408,7 +19213,7 @@ msgstr "スレッドのおおよそのメンバー数。この値は50の上限 msgid "The raw data given by the :ddocs:`gateway `." msgstr ":ddocs:`ゲートウェイ ` によって与えられた生のデータ。" -#: ../../api.rst:4693 +#: ../../api.rst:4943 msgid "RawThreadDeleteEvent" msgstr "RawThreadDeleteEvent" @@ -18432,7 +19237,7 @@ msgstr "スレッドが削除されたギルドのID。" msgid "The ID of the channel the thread belonged to." msgstr "スレッドが属したチャンネルの ID。" -#: ../../api.rst:4701 +#: ../../api.rst:4951 msgid "RawTypingEvent" msgstr "RawTypingEvent" @@ -18460,7 +19265,7 @@ msgstr "Optional[Union[:class:`discord.User`, :class:`discord.Member`]]" msgid "The ID of the guild the user started typing in, if applicable." msgstr "該当する場合、ユーザーが入力し始めたギルドのID。" -#: ../../api.rst:4709 +#: ../../api.rst:4959 msgid "RawMemberRemoveEvent" msgstr "RawMemberRemoveEvent" @@ -18480,7 +19285,7 @@ msgstr "Union[:class:`discord.User`, :class:`discord.Member`]" msgid "The ID of the guild the user left." msgstr "ユーザーが脱退したギルドのID。" -#: ../../api.rst:4717 +#: ../../api.rst:4967 msgid "RawAppCommandPermissionsUpdateEvent" msgstr "RawAppCommandPermissionsUpdateEvent" @@ -18504,7 +19309,7 @@ msgstr "権限が更新されたギルド。" msgid "List of new permissions for the app command." msgstr "アプリケーションコマンドの新しい権限のリスト。" -#: ../../api.rst:4725 +#: ../../api.rst:4975 msgid "PartialWebhookGuild" msgstr "PartialWebhookGuild" @@ -18517,7 +19322,7 @@ msgstr "Webhook用の部分的なギルドを表します。" msgid "These are typically given for channel follower webhooks." msgstr "これは通常、チャンネルをフォローするWebhookから与えられます。" -#: ../../api.rst:4733 +#: ../../api.rst:4983 msgid "PartialWebhookChannel" msgstr "PartialWebhookChannel" @@ -18525,23 +19330,23 @@ msgstr "PartialWebhookChannel" msgid "Represents a partial channel for webhooks." msgstr "Webhook用の部分的なチャンネルを表します。" -#: ../../api.rst:4743 +#: ../../api.rst:4993 msgid "Data Classes" msgstr "データクラス" -#: ../../api.rst:4745 +#: ../../api.rst:4995 msgid "Some classes are just there to be data containers, this lists them." msgstr "一部のクラスはデータコンテナとして用いられます。ここではそのクラスを一覧表にしています。" -#: ../../api.rst:4747 +#: ../../api.rst:4997 msgid "Unlike :ref:`models ` you are allowed to create most of these yourself, even if they can also be used to hold attributes." msgstr ":ref:`models ` とは異なり、属性を持つものであっても、自分で作成することが許されています。" -#: ../../api.rst:4753 +#: ../../api.rst:5003 msgid "The only exception to this rule is :class:`Object`, which is made with dynamic attributes in mind." msgstr "このルールの唯一の例外は :class:`Object` で、動的な属性を念頭に置いて作成されます。" -#: ../../api.rst:4758 +#: ../../api.rst:5008 msgid "Object" msgstr "Object" @@ -18589,7 +19394,7 @@ msgstr "Type[:class:`abc.Snowflake`]" msgid "Returns the snowflake's creation time in UTC." msgstr "スノーフレークの作成時刻をUTCで返します。" -#: ../../api.rst:4766 +#: ../../api.rst:5016 msgid "Embed" msgstr "Embed" @@ -18902,7 +19707,7 @@ msgstr "無効なインデックスが指定された場合。" msgid "Converts this embed object into a dict." msgstr "埋め込みオブジェクトを辞書型に変換します。" -#: ../../api.rst:4774 +#: ../../api.rst:5024 msgid "AllowedMentions" msgstr "AllowedMentions" @@ -18943,7 +19748,7 @@ msgstr "すべてのフィールドが ``True`` に明示的に設定された : msgid "A factory method that returns a :class:`AllowedMentions` with all fields set to ``False``" msgstr "すべてのフィールドが ``False`` に設定された :class:`AllowedMentions` を返すファクトリメソッド。" -#: ../../api.rst:4782 +#: ../../api.rst:5032 msgid "MessageReference" msgstr "MessageReference" @@ -19008,7 +19813,7 @@ msgstr "Optional[:class:`~discord.Message`]" msgid "Returns a URL that allows the client to jump to the referenced message." msgstr "クライアントが参照されたメッセージにジャンプすることのできるURLを返します。" -#: ../../api.rst:4790 +#: ../../api.rst:5040 msgid "PartialMessage" msgstr "PartialMessage" @@ -19068,7 +19873,11 @@ msgstr "該当する場合、この部分的なメッセージが属するギル msgid "The partial message's creation time in UTC." msgstr "UTCの、部分的なメッセージが作成された時刻。" -#: ../../api.rst:4798 +#: ../../../discord/message.py:docstring of discord.PartialMessage.thread:5 +msgid "This does not retrieve archived threads, as they are not retained in the internal cache. Use :meth:`fetch_thread` instead." +msgstr "" + +#: ../../api.rst:5048 msgid "MessageApplication" msgstr "MessageApplication" @@ -19084,7 +19893,7 @@ msgstr "存在する場合、アプリケーションのアイコン。" msgid "The application's cover image, if any." msgstr "存在する場合、アプリケーションのカバー画像。" -#: ../../api.rst:4806 +#: ../../api.rst:5056 msgid "RoleSubscriptionInfo" msgstr "RoleSubscriptionInfo" @@ -19112,7 +19921,7 @@ msgstr "ユーザーが購読している月数の合計。" msgid "Whether this notification is for a renewal rather than a new purchase." msgstr "この通知が新しい購入ではなく、更新のためであるかどうか。" -#: ../../api.rst:4814 +#: ../../api.rst:5064 msgid "Intents" msgstr "Intents" @@ -19894,7 +20703,7 @@ msgstr "自動管理ルール対応関係のイベントが有効になってい msgid "This corresponds to the following events: - :func:`on_automod_action`" msgstr "これは以下のイベントに対応します: - :func:`on_automod_action`" -#: ../../api.rst:4822 +#: ../../api.rst:5072 msgid "MemberCacheFlags" msgstr "MemberCacheFlags" @@ -19986,7 +20795,7 @@ msgstr "結果として生成されるメンバーキャッシュフラグ。" msgid ":class:`MemberCacheFlags`" msgstr ":class:`MemberCacheFlags`" -#: ../../api.rst:4830 +#: ../../api.rst:5080 msgid "ApplicationFlags" msgstr "ApplicationFlags" @@ -20070,7 +20879,7 @@ msgstr "アプリケーションがグローバルアプリケーションコマ msgid "Returns ``True`` if the application has had at least one global application command used in the last 30 days." msgstr "過去30日間で少なくとも1つのグローバルアプリケーションコマンドが使用されている場合に ``True`` を返します。" -#: ../../api.rst:4838 +#: ../../api.rst:5088 msgid "ChannelFlags" msgstr "ChannelFlags" @@ -20110,7 +20919,11 @@ msgstr "スレッドがフォーラムチャンネルにピン留めされてい msgid "Returns ``True`` if a tag is required to be specified when creating a thread in a :class:`ForumChannel`." msgstr ":class:`ForumChannel` でスレッドを作成する際にタグを指定する必要がある場合に ``True`` を返します。" -#: ../../api.rst:4846 +#: ../../docstring of discord.ChannelFlags.hide_media_download_options:1 +msgid "Returns ``True`` if the client hides embedded media download options in a :class:`ForumChannel`. Only available in media channels." +msgstr "" + +#: ../../api.rst:5096 msgid "AutoModPresets" msgstr "AutoModPresets" @@ -20162,7 +20975,7 @@ msgstr "すべて有効化された :class:`AutoModPresets` を作成するフ msgid "A factory method that creates a :class:`AutoModPresets` with everything disabled." msgstr "すべて無効化された :class:`AutoModPresets` を作成するファクトリメソッド。" -#: ../../api.rst:4854 +#: ../../api.rst:5104 msgid "AutoModRuleAction" msgstr "AutoModRuleAction" @@ -20198,7 +21011,7 @@ msgstr "Optional[:class:`datetime.timedelta`]" msgid "A custom message which will be shown to a user when their message is blocked. Passing this sets :attr:`type` to :attr:`~AutoModRuleActionType.block_message`." msgstr "メッセージがブロックされたときに送信者に表示されるカスタムメッセージ。 :attr:`type` を :attr:`~AutoModRuleActionType.block_message` に設定します。" -#: ../../api.rst:4862 +#: ../../api.rst:5112 msgid "AutoModTrigger" msgstr "AutoModTrigger" @@ -20223,6 +21036,7 @@ msgid ":attr:`AutoModRuleTriggerType.keyword`" msgstr ":attr:`AutoModRuleTriggerType.keyword`" #: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:8 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:18 msgid ":attr:`keyword_filter`, :attr:`regex_patterns`, :attr:`allow_list`" msgstr ":attr:`keyword_filter`, :attr:`regex_patterns`, :attr:`allow_list`" @@ -20243,46 +21057,54 @@ msgid ":attr:`AutoModRuleTriggerType.mention_spam`" msgstr ":attr:`AutoModRuleTriggerType.mention_spam`" #: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:15 -msgid ":attr:`mention_limit`" -msgstr ":attr:`mention_limit`" +msgid ":attr:`mention_limit`, :attr:`mention_raid_protection`" +msgstr "" + +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:18 +msgid ":attr:`AutoModRuleTriggerType.member_profile`" +msgstr "" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:22 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:26 msgid "The type of trigger." msgstr "発動条件の種類。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:28 -msgid "The list of strings that will trigger the keyword filter. Maximum of 1000. Keywords can only be up to 60 characters in length." -msgstr "キーワードフィルタを発動させる文字列の一覧。最大1000個まで。キーワードは各60文字以内です。" +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:32 +msgid "The list of strings that will trigger the filter. Maximum of 1000. Keywords can only be up to 60 characters in length." +msgstr "" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:31 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:35 msgid "This could be combined with :attr:`regex_patterns`." msgstr ":attr:`regex_patterns` と組み合わせることができます。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:37 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:41 msgid "The regex pattern that will trigger the filter. The syntax is based off of `Rust's regex syntax `_. Maximum of 10. Regex strings can only be up to 260 characters in length." msgstr "フィルタを発動させる正規表現パターン。構文は `Rust の正規表現構文 `_ に基づいています。 最大 10 個まで。正規表現文字列は 260 文字までしか使用できません。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:41 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:45 msgid "This could be combined with :attr:`keyword_filter` and/or :attr:`allow_list`" msgstr ":attr:`keyword_filter` や :attr:`allow_list` と組み合わせることができます。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:49 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:53 msgid "The presets used with the preset keyword filter." msgstr "プリセットキーワードフィルタで使用されるプリセット。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:51 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:55 msgid ":class:`AutoModPresets`" msgstr ":class:`AutoModPresets`" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:55 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:59 msgid "The list of words that are exempt from the commonly flagged words. Maximum of 100. Keywords can only be up to 60 characters in length." msgstr "共通のキーワードフィルタの単語から除外される単語の一覧。最大100個まで。キーワードは各60文字以内です。" -#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:62 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:66 msgid "The total number of user and role mentions a message can contain. Has a maximum of 50." msgstr "メッセージに含めることのできるユーザーとロールのメンションの合計数。最大で50個です。" -#: ../../api.rst:4870 +#: ../../../discord/automod.py:docstring of discord.automod.AutoModTrigger:73 +msgid "Whether mention raid protection is enabled or not." +msgstr "" + +#: ../../api.rst:5120 msgid "File" msgstr "File" @@ -20322,7 +21144,7 @@ msgstr "表示するファイルの説明。現在画像でのみサポートさ msgid "The filename to display when uploading to Discord. If this is not given then it defaults to ``fp.name`` or if ``fp`` is a string then the ``filename`` will default to the string given." msgstr "Discordにアップロードするときに表示されるファイル名。指定されていない場合はデフォルトでは ``fp.name`` 、または ``fp`` が文字列の場合、 ``filename`` は与えられた文字列をデフォルトにします。" -#: ../../api.rst:4878 +#: ../../api.rst:5128 msgid "Colour" msgstr "Colour" @@ -20570,7 +21392,7 @@ msgstr "``0xEEEFF1`` の値を持つ :class:`Colour` を返すクラスメソッ msgid "A factory method that returns a :class:`Colour` with a value of ``0xEB459F``." msgstr "``0xEB459F`` の値を持つ :class:`Colour` を返すクラスメソッドです。" -#: ../../api.rst:4886 +#: ../../api.rst:5136 msgid "BaseActivity" msgstr "BaseActivity" @@ -20608,7 +21430,7 @@ msgstr "なお、ライブラリはこれらをユーザー設定可能としま msgid "When the user started doing this activity in UTC." msgstr "ユーザーがアクティビティを開始したときのUTC時刻。" -#: ../../api.rst:4894 +#: ../../api.rst:5144 msgid "Activity" msgstr "Activity" @@ -20649,54 +21471,62 @@ msgid "The detail of the user's current activity." msgstr "ユーザーの現在のアクティビティの詳細。" #: ../../../discord/activity.py:docstring of discord.activity.Activity:50 +msgid "The user's current platform." +msgstr "" + +#: ../../../discord/activity.py:docstring of discord.activity.Activity:58 msgid "A dictionary of timestamps. It contains the following optional keys:" msgstr "タイムスタンプの辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:52 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:60 msgid "``start``: Corresponds to when the user started doing the activity in milliseconds since Unix epoch." msgstr "``start``: ユーザーがアクティビティを開始したときのUnixエポック起算ミリ秒数に対応します。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:54 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:62 msgid "``end``: Corresponds to when the user will finish doing the activity in milliseconds since Unix epoch." msgstr "``end``: ユーザーがアクティビティを終了する予定時刻のUnixエポック起算ミリ秒数に対応します。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:61 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:69 msgid "A dictionary representing the images and their hover text of an activity. It contains the following optional keys:" msgstr "アクティビティの画像とそれらのホバーテキストを表す辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:64 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:72 +#: ../../../discord/activity.py:docstring of discord.activity.Game:45 msgid "``large_image``: A string representing the ID for the large image asset." msgstr "``large_image``: 大きな画像アセットのIDを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:65 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:73 +#: ../../../discord/activity.py:docstring of discord.activity.Game:46 msgid "``large_text``: A string representing the text when hovering over the large image asset." msgstr "``large_text``: 大きな画像アセットをホバーしたときに表示するテキストを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:66 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:74 +#: ../../../discord/activity.py:docstring of discord.activity.Game:47 msgid "``small_image``: A string representing the ID for the small image asset." msgstr "``small_image``: 小さな画像アセットのIDを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:67 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:75 +#: ../../../discord/activity.py:docstring of discord.activity.Game:48 msgid "``small_text``: A string representing the text when hovering over the small image asset." msgstr "``small_text``: 小さな画像アセットをホバーしたときに表示するテキストを表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:73 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:81 msgid "A dictionary representing the activity party. It contains the following optional keys:" msgstr "アクティビティのパーティーを表す辞書。次のオプションキーが含まれています:" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:75 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:83 msgid "``id``: A string representing the party ID." msgstr "``id``: パーティー ID を表す文字列。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:76 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:84 msgid "``size``: A list of up to two integer elements denoting (current_size, maximum_size)." msgstr "``size``: 現在の大きさと最大の大きさをである二個以内の整数のリスト。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:82 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:90 msgid "A list of strings representing the labels of custom buttons shown in a rich presence." msgstr "リッチプレゼンスに表示されるカスタムボタンのラベルを表す文字列のリスト。" -#: ../../../discord/activity.py:docstring of discord.activity.Activity:90 +#: ../../../discord/activity.py:docstring of discord.activity.Activity:98 msgid "The emoji that belongs to this activity." msgstr "このアクティビティに属する絵文字。" @@ -20724,7 +21554,7 @@ msgstr "該当する場合、このアクティビティの大きな画像アセ msgid "Returns the small image asset hover text of this activity, if applicable." msgstr "該当する場合、このアクティビティの小さな画像アセットのホバーテキストを返します。" -#: ../../api.rst:4902 +#: ../../api.rst:5152 msgid "Game" msgstr "Game" @@ -20757,6 +21587,14 @@ msgstr "ゲームの名前を返します。" msgid "The game's name." msgstr "ゲームの名前。" +#: ../../../discord/activity.py:docstring of discord.activity.Game:34 +msgid "Where the user is playing from (ie. PS5, Xbox)." +msgstr "" + +#: ../../../discord/activity.py:docstring of discord.activity.Game:42 +msgid "A dictionary representing the images and their hover text of a game. It contains the following optional keys:" +msgstr "" + #: ../../../discord/activity.py:docstring of discord.Game.type:1 #: ../../../discord/activity.py:docstring of discord.Streaming.type:1 msgid "Returns the game's type. This is for compatibility with :class:`Activity`." @@ -20774,7 +21612,7 @@ msgstr "該当する場合、ユーザーがゲームを開始したときのUTC msgid "When the user will stop playing this game in UTC, if applicable." msgstr "該当する場合、ユーザーがゲームを終了する予定のUTC時刻。" -#: ../../api.rst:4910 +#: ../../api.rst:5160 msgid "Streaming" msgstr "Streaming" @@ -20838,7 +21676,7 @@ msgstr "提供された場合、ストリーム中のユーザーのTwitchの名 msgid "This corresponds to the ``large_image`` key of the :attr:`Streaming.assets` dictionary if it starts with ``twitch:``. Typically set by the Discord client." msgstr "これが ``twitch:`` で始まる場合、 :attr:`Streaming.assets` 辞書の ``large_image`` キーに対応します。典型的にはDiscordクライアントによって設定されます。" -#: ../../api.rst:4918 +#: ../../api.rst:5168 msgid "CustomActivity" msgstr "CustomActivity" @@ -20862,7 +21700,7 @@ msgstr "存在する場合、アクティビティに渡す絵文字。" msgid "It always returns :attr:`ActivityType.custom`." msgstr "これは常に :attr:`ActivityType.custom` を返します。" -#: ../../api.rst:4926 +#: ../../api.rst:5176 msgid "Permissions" msgstr "Permissions" @@ -21105,6 +21943,10 @@ msgstr ":attr:`manage_threads`" msgid ":attr:`moderate_members`" msgstr ":attr:`moderate_members`" +#: ../../../discord/permissions.py:docstring of discord.permissions.Permissions.events:1 +msgid "A factory method that creates a :class:`Permissions` with all \"Events\" permissions from the official Discord UI set to ``True``." +msgstr "" + #: ../../../discord/permissions.py:docstring of discord.permissions.Permissions.advanced:1 msgid "A factory method that creates a :class:`Permissions` with all \"Advanced\" permissions from the official Discord UI set to ``True``." msgstr "Discord公式UIの「高度な権限」をすべて ``True`` に設定した :class:`Permissions` を作成するファクトリメソッド。" @@ -21323,6 +22165,10 @@ msgstr "ユーザーがボイスチャンネルにて埋め込みアプリケー msgid "Returns ``True`` if a user can time out other members." msgstr "ユーザーが他のユーザーをタイムアウトできる場合は ``True`` を返します。" +#: ../../docstring of discord.Permissions.view_creator_monetization_analytics:1 +msgid "Returns ``True`` if a user can view role subscription insights." +msgstr "" + #: ../../docstring of discord.Permissions.use_soundboard:1 msgid "Returns ``True`` if a user can use the soundboard." msgstr "ユーザーがサウンドボードを使用できる場合は ``True`` を返します。" @@ -21331,6 +22177,10 @@ msgstr "ユーザーがサウンドボードを使用できる場合は ``True`` msgid "Returns ``True`` if a user can create emojis, stickers, and soundboard sounds." msgstr "絵文字、スタンプ、サウンドボードのサウンドを作成できる場合は ``True`` を返します。" +#: ../../docstring of discord.Permissions.create_events:1 +msgid "Returns ``True`` if a user can create guild events." +msgstr "" + #: ../../docstring of discord.Permissions.use_external_sounds:1 msgid "Returns ``True`` if a user can use sounds from other guilds." msgstr "ユーザーが他のギルドのサウンドを使用できる場合は ``True`` を返します。" @@ -21339,7 +22189,7 @@ msgstr "ユーザーが他のギルドのサウンドを使用できる場合は msgid "Returns ``True`` if a user can send voice messages." msgstr "ユーザーがボイスメッセージを送信できる場合は ``True`` を返します。" -#: ../../api.rst:4934 +#: ../../api.rst:5184 msgid "PermissionOverwrite" msgstr "PermissionOverwrite" @@ -21395,7 +22245,7 @@ msgstr "権限上書きオブジェクトを一括更新します。" msgid "A list of key/value pairs to bulk update with." msgstr "一括更新するためのキーと値のペアのリスト。" -#: ../../api.rst:4942 +#: ../../api.rst:5192 msgid "SystemChannelFlags" msgstr "SystemChannelFlags" @@ -21453,7 +22303,7 @@ msgstr "ロールサブスクリプションの購入と更新通知が有効に msgid "Returns ``True`` if the role subscription notifications have a sticker reply button." msgstr "ロールサブスクリプション通知にスタンプの返信ボタンがある場合に ``True`` を返します。" -#: ../../api.rst:4950 +#: ../../api.rst:5200 msgid "MessageFlags" msgstr "MessageFlags" @@ -21533,7 +22383,7 @@ msgstr ":attr:`suppress_notifications` のエイリアス。" msgid "Returns ``True`` if the message is a voice message." msgstr "メッセージがボイスメッセージの場合に ``True`` を返します。" -#: ../../api.rst:4958 +#: ../../api.rst:5208 msgid "PublicUserFlags" msgstr "PublicUserFlags" @@ -21641,7 +22491,7 @@ msgstr "ユーザーがアクティブな開発者の場合に ``True`` を返 msgid "List[:class:`UserFlags`]: Returns all public flags the user has." msgstr "List[:class:`UserFlags`]: ユーザーが持つすべての公開フラグを返します。" -#: ../../api.rst:4966 +#: ../../api.rst:5216 msgid "MemberFlags" msgstr "MemberFlags" @@ -21689,7 +22539,131 @@ msgstr "メンバーがギルドの認証要件をバイパスできる場合に msgid "Returns ``True`` if the member has started onboarding." msgstr "メンバーがオンボーディングを開始した場合に ``True`` を返します。" -#: ../../api.rst:4974 +#: ../../api.rst:5224 +msgid "AttachmentFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:1 +msgid "Wraps up the Discord Attachment flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:9 +msgid "Checks if two AttachmentFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:13 +msgid "Checks if two AttachmentFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:17 +msgid "Returns a AttachmentFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:22 +msgid "Returns a AttachmentFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:27 +msgid "Returns a AttachmentFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.AttachmentFlags:32 +msgid "Returns a AttachmentFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.clip:1 +msgid "Returns ``True`` if the attachment is a clip." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.thumbnail:1 +msgid "Returns ``True`` if the attachment is a thumbnail." +msgstr "" + +#: ../../docstring of discord.AttachmentFlags.remix:1 +msgid "Returns ``True`` if the attachment has been edited using the remix feature." +msgstr "" + +#: ../../api.rst:5232 +msgid "RoleFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:1 +msgid "Wraps up the Discord Role flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:9 +msgid "Checks if two RoleFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:13 +msgid "Checks if two RoleFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:17 +msgid "Returns a RoleFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:22 +msgid "Returns a RoleFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:27 +msgid "Returns a RoleFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.RoleFlags:32 +msgid "Returns a RoleFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.RoleFlags.in_prompt:1 +msgid "Returns ``True`` if the role can be selected by members in an onboarding prompt." +msgstr "" + +#: ../../api.rst:5240 +msgid "SKUFlags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:1 +msgid "Wraps up the Discord SKU flags" +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:9 +msgid "Checks if two SKUFlags are equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:13 +msgid "Checks if two SKUFlags are not equal." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:17 +msgid "Returns a SKUFlags instance with all enabled flags from both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:22 +msgid "Returns a SKUFlags instance with only flags enabled on both x and y." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:27 +msgid "Returns a SKUFlags instance with only flags enabled on only one of x or y, not on both." +msgstr "" + +#: ../../../discord/flags.py:docstring of discord.flags.SKUFlags:32 +msgid "Returns a SKUFlags instance with all flags inverted from x." +msgstr "" + +#: ../../docstring of discord.SKUFlags.available:1 +msgid "Returns ``True`` if the SKU is available for purchase." +msgstr "" + +#: ../../docstring of discord.SKUFlags.guild_subscription:1 +msgid "Returns ``True`` if the SKU is a guild subscription." +msgstr "" + +#: ../../docstring of discord.SKUFlags.user_subscription:1 +msgid "Returns ``True`` if the SKU is a user subscription." +msgstr "" + +#: ../../api.rst:5248 msgid "ForumTag" msgstr "ForumTag" @@ -21725,11 +22699,11 @@ msgstr ":attr:`~Permissions.manage_threads` 権限を有するモデレータの msgid "The emoji that is used to represent this tag. Note that if the emoji is a custom emoji, it will *not* have name information." msgstr "このタグを表すために使用される絵文字。絵文字がカスタム絵文字の場合、名前情報は *提供されません* 。" -#: ../../api.rst:4983 +#: ../../api.rst:5257 msgid "Exceptions" msgstr "例外" -#: ../../api.rst:4985 +#: ../../api.rst:5259 msgid "The following exceptions are thrown by the library." msgstr "以下の例外がライブラリにより送出されます。" @@ -21887,67 +22861,67 @@ msgstr "返されたエラーコード。" msgid "An exception that is thrown for when libopus is not loaded." msgstr "libopus がロードされていないときに送出される例外。" -#: ../../api.rst:5020 +#: ../../api.rst:5294 msgid "Exception Hierarchy" msgstr "例外の階層構造" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`Exception`" msgstr ":exc:`Exception`" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`DiscordException`" msgstr ":exc:`DiscordException`" -#: ../../api.rst:5030 +#: ../../api.rst:5304 msgid ":exc:`ClientException`" msgstr ":exc:`ClientException`" -#: ../../api.rst:5027 +#: ../../api.rst:5301 msgid ":exc:`InvalidData`" msgstr ":exc:`InvalidData`" -#: ../../api.rst:5028 +#: ../../api.rst:5302 msgid ":exc:`LoginFailure`" msgstr ":exc:`LoginFailure`" -#: ../../api.rst:5029 +#: ../../api.rst:5303 msgid ":exc:`ConnectionClosed`" msgstr ":exc:`ConnectionClosed`" -#: ../../api.rst:5030 +#: ../../api.rst:5304 msgid ":exc:`PrivilegedIntentsRequired`" msgstr ":exc:`PrivilegedIntentsRequired`" -#: ../../api.rst:5031 +#: ../../api.rst:5305 msgid ":exc:`InteractionResponded`" msgstr ":exc:`InteractionResponded`" -#: ../../api.rst:5032 +#: ../../api.rst:5306 msgid ":exc:`GatewayNotFound`" msgstr ":exc:`GatewayNotFound`" -#: ../../api.rst:5036 +#: ../../api.rst:5310 msgid ":exc:`HTTPException`" msgstr ":exc:`HTTPException`" -#: ../../api.rst:5034 +#: ../../api.rst:5308 msgid ":exc:`Forbidden`" msgstr ":exc:`Forbidden`" -#: ../../api.rst:5035 +#: ../../api.rst:5309 msgid ":exc:`NotFound`" msgstr ":exc:`NotFound`" -#: ../../api.rst:5036 +#: ../../api.rst:5310 msgid ":exc:`DiscordServerError`" msgstr ":exc:`DiscordServerError`" -#: ../../api.rst:5037 +#: ../../api.rst:5311 msgid ":exc:`app_commands.CommandSyncFailure`" msgstr ":exc:`app_commands.CommandSyncFailure`" -#: ../../api.rst:5038 +#: ../../api.rst:5312 msgid ":exc:`RateLimited`" msgstr ":exc:`RateLimited`" diff --git a/docs/locale/ja/LC_MESSAGES/discord.po b/docs/locale/ja/LC_MESSAGES/discord.po index 55d1bdc6bbd9..66070b699c7a 100644 --- a/docs/locale/ja/LC_MESSAGES/discord.po +++ b/docs/locale/ja/LC_MESSAGES/discord.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po b/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po index 4099131ca160..07125ecc4a7d 100644 --- a/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po +++ b/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/faq.po b/docs/locale/ja/LC_MESSAGES/faq.po index c92e82a4a42d..4fb1dddff903 100644 --- a/docs/locale/ja/LC_MESSAGES/faq.po +++ b/docs/locale/ja/LC_MESSAGES/faq.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/intents.po b/docs/locale/ja/LC_MESSAGES/intents.po index ab439dba0661..907f1e79e72f 100644 --- a/docs/locale/ja/LC_MESSAGES/intents.po +++ b/docs/locale/ja/LC_MESSAGES/intents.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/intro.po b/docs/locale/ja/LC_MESSAGES/intro.po index ca6937854784..93acdbe80f85 100644 --- a/docs/locale/ja/LC_MESSAGES/intro.po +++ b/docs/locale/ja/LC_MESSAGES/intro.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/logging.po b/docs/locale/ja/LC_MESSAGES/logging.po index 05ef05165c20..d5a6b114dcb6 100644 --- a/docs/locale/ja/LC_MESSAGES/logging.po +++ b/docs/locale/ja/LC_MESSAGES/logging.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -46,18 +46,22 @@ msgid "This is recommended, especially at verbose levels such as ``DEBUG``, as t msgstr "特に、 ``DEBUG`` といった冗長なイベントレベルを設定している場合、プログラムの標準エラー出力をつまらせてしまう原因になるため、ファイルへの出力が推奨されます。" #: ../../logging.rst:46 +msgid "If you want the logging configuration the library provides to affect all loggers rather than just the ``discord`` logger, you can pass ``root_logger=True`` inside :meth:`Client.run`:" +msgstr "" + +#: ../../logging.rst:52 msgid "If you want to setup logging using the library provided configuration without using :meth:`Client.run`, you can use :func:`discord.utils.setup_logging`:" msgstr ":meth:`Client.run` を使用せずにライブラリ提供の構成を使用して logging を設定したい場合は、 :func:`discord.utils.setup_logging` を使用できます。" -#: ../../logging.rst:57 +#: ../../logging.rst:63 msgid "More advanced setups are possible with the :mod:`logging` module. The example below configures a rotating file handler that outputs DEBUG output for everything the library outputs, except for HTTP requests:" msgstr ":mod:`logging` モジュールを使用するとより高度なセットアップが行えます。以下の例では、HTTPリクエスト以外のすべてのライブラリの出力に対しDEBUG出力を使用するローテーションを行うファイルハンドラを構成します。" -#: ../../logging.rst:85 +#: ../../logging.rst:91 msgid "For more information, check the documentation and tutorial of the :mod:`logging` module." msgstr "詳細は、:mod:`logging` モジュールのドキュメントを参照してください。" -#: ../../logging.rst:89 +#: ../../logging.rst:95 msgid "The library now provides a default logging configuration." msgstr "ライブラリがデフォルト logging 構成を提供するようになりました。" diff --git a/docs/locale/ja/LC_MESSAGES/migrating.po b/docs/locale/ja/LC_MESSAGES/migrating.po index 1d60b6898c70..9dad600072ff 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating.po +++ b/docs/locale/ja/LC_MESSAGES/migrating.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/migrating_to_async.po b/docs/locale/ja/LC_MESSAGES/migrating_to_async.po index 250ee432e3cd..cce2227c2a71 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating_to_async.po +++ b/docs/locale/ja/LC_MESSAGES/migrating_to_async.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po b/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po index 4b104b094717..64c88c2cb096 100644 --- a/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po +++ b/docs/locale/ja/LC_MESSAGES/migrating_to_v1.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/quickstart.po b/docs/locale/ja/LC_MESSAGES/quickstart.po index 4037bde71d88..6befcd7c0e47 100644 --- a/docs/locale/ja/LC_MESSAGES/quickstart.po +++ b/docs/locale/ja/LC_MESSAGES/quickstart.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/sphinx.po b/docs/locale/ja/LC_MESSAGES/sphinx.po index d50bed60dabc..eceded7f8198 100644 --- a/docs/locale/ja/LC_MESSAGES/sphinx.po +++ b/docs/locale/ja/LC_MESSAGES/sphinx.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/version_guarantees.po b/docs/locale/ja/LC_MESSAGES/version_guarantees.po index 3d45729be206..a0e5bc0970c0 100644 --- a/docs/locale/ja/LC_MESSAGES/version_guarantees.po +++ b/docs/locale/ja/LC_MESSAGES/version_guarantees.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" diff --git a/docs/locale/ja/LC_MESSAGES/whats_new.po b/docs/locale/ja/LC_MESSAGES/whats_new.po index e849eb8485f7..0a75ffd174a1 100644 --- a/docs/locale/ja/LC_MESSAGES/whats_new.po +++ b/docs/locale/ja/LC_MESSAGES/whats_new.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: discordpy\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-21 01:17+0000\n" -"PO-Revision-Date: 2023-10-30 15:32\n" +"POT-Creation-Date: 2024-03-26 03:41+0000\n" +"PO-Revision-Date: 2024-04-17 02:43\n" "Last-Translator: \n" "Language-Team: Japanese\n" "MIME-Version: 1.0\n" @@ -26,3407 +26,3471 @@ msgid "This page keeps a detailed human friendly rendering of what's new and cha msgstr "このページでは、特定のバージョンの新機能や変更された機能をわかりやすい形で詳細に記載しています。" #: ../../whats_new.rst:17 +msgid "v2.3.2" +msgstr "" + +#: ../../whats_new.rst:20 +#: ../../whats_new.rst:37 +#: ../../whats_new.rst:91 +#: ../../whats_new.rst:125 +#: ../../whats_new.rst:139 +msgid "Bug Fixes" +msgstr "バグ修正" + +#: ../../whats_new.rst:22 +msgid "Fix the ``name`` parameter not being respected when sending a :class:`CustomActivity`." +msgstr "" + +#: ../../whats_new.rst:23 +msgid "Fix :attr:`Intents.emoji` and :attr:`Intents.emojis_and_stickers` having swapped alias values (:issue:`9471`)." +msgstr "" + +#: ../../whats_new.rst:24 +msgid "Fix ``NameError`` when using :meth:`abc.GuildChannel.create_invite` (:issue:`9505`)." +msgstr "" + +#: ../../whats_new.rst:25 +msgid "Fix crash when disconnecting during the middle of a ``HELLO`` packet when using :class:`AutoShardedClient`." +msgstr "" + +#: ../../whats_new.rst:26 +msgid "Fix overly eager escape behaviour for lists and header markdown in :func:`utils.escape_markdown` (:issue:`9516`)." +msgstr "" + +#: ../../whats_new.rst:27 +msgid "Fix voice websocket not being closed before being replaced by a new one (:issue:`9518`)." +msgstr "" + +#: ../../whats_new.rst:28 +msgid "|commands| Fix the wrong :meth:`~ext.commands.HelpCommand.on_help_command_error` being called when ejected from a cog." +msgstr "" + +#: ../../whats_new.rst:29 +msgid "|commands| Fix ``=None`` being displayed in :attr:`~ext.commands.Command.signature`." +msgstr "" + +#: ../../whats_new.rst:34 +msgid "v2.3.1" +msgstr "" + +#: ../../whats_new.rst:39 +msgid "Fix username lookup in :meth:`Guild.get_member_named` (:issue:`9451`)." +msgstr "" + +#: ../../whats_new.rst:41 +msgid "Use cache data first for :attr:`Interaction.channel` instead of API data." +msgstr "" + +#: ../../whats_new.rst:41 +msgid "This bug usually manifested in incomplete channel objects (e.g. no ``overwrites``) because Discord does not provide this data." +msgstr "" + +#: ../../whats_new.rst:43 +msgid "Fix false positives in :meth:`PartialEmoji.from_str` inappropriately setting ``animated`` to ``True`` (:issue:`9456`, :issue:`9457`)." +msgstr "" + +#: ../../whats_new.rst:44 +msgid "Fix certain select types not appearing in :attr:`Message.components` (:issue:`9462`)." +msgstr "" + +#: ../../whats_new.rst:45 +msgid "|commands| Change lookup order for :class:`~ext.commands.MemberConverter` and :class:`~ext.commands.UserConverter` to prioritise usernames instead of nicknames." +msgstr "" + +#: ../../whats_new.rst:50 msgid "v2.3.0" msgstr "v2.3.0" -#: ../../whats_new.rst:20 -#: ../../whats_new.rst:116 -#: ../../whats_new.rst:206 -#: ../../whats_new.rst:327 -#: ../../whats_new.rst:406 +#: ../../whats_new.rst:53 +#: ../../whats_new.rst:149 +#: ../../whats_new.rst:239 +#: ../../whats_new.rst:360 +#: ../../whats_new.rst:439 msgid "New Features" msgstr "新機能" -#: ../../whats_new.rst:28 +#: ../../whats_new.rst:61 msgid "Add support for the new username system (also known as \"pomelo\")." msgstr "新しいユーザー名システム (\"pomelo\"とも呼ばれます) のサポートを追加しました。" -#: ../../whats_new.rst:23 +#: ../../whats_new.rst:56 msgid "Add :attr:`User.global_name` to get their global nickname or \"display name\"." msgstr "グローバルのニックネーム、つまり「表示名」を取得する :attr:`User.global_name` を追加しました。" -#: ../../whats_new.rst:24 +#: ../../whats_new.rst:57 msgid "Update :attr:`User.display_name` and :attr:`Member.display_name` to understand global nicknames." msgstr ":attr:`User.display_name` と :attr:`Member.display_name` を、グローバルのニックネームを使用するように変更しました。" -#: ../../whats_new.rst:25 +#: ../../whats_new.rst:58 msgid "Update ``__str__`` for :class:`User` to drop discriminators if the user has been migrated." msgstr ":class:`User` の ``__str__`` が、移行したユーザーのタグを含まないよう、変更しました。" -#: ../../whats_new.rst:26 +#: ../../whats_new.rst:59 msgid "Update :meth:`Guild.get_member_named` to work with migrated users." msgstr "移行したユーザーでも動くよう :meth:`Guild.get_member_named` を変更しました。" -#: ../../whats_new.rst:27 +#: ../../whats_new.rst:60 msgid "Update :attr:`User.default_avatar` to work with migrated users." msgstr "移行したユーザーでも動くよう :attr:`User.default_avatar` を変更しました。" -#: ../../whats_new.rst:28 +#: ../../whats_new.rst:61 msgid "|commands| Update user and member converters to understand migrated users." msgstr "|commands| 移行したユーザーを解釈するよう、ユーザーとメンバーコンバータを変更しました。" -#: ../../whats_new.rst:30 +#: ../../whats_new.rst:63 msgid "Add :attr:`DefaultAvatar.pink` for new pink default avatars." msgstr "新しいピンクのデフォルトアバタ―用の :attr:`DefaultAvatar.pink` を追加しました。" -#: ../../whats_new.rst:31 +#: ../../whats_new.rst:64 msgid "Add :meth:`Colour.pink` to get the pink default avatar colour." msgstr "ピンクのデフォルトアバターの色を取得する :meth:`Colour.pink` を追加しました。" -#: ../../whats_new.rst:36 +#: ../../whats_new.rst:69 msgid "Add support for voice messages (:issue:`9358`)" msgstr "ボイスメッセージのサポートを追加しました。 (:issue:`9358`)" -#: ../../whats_new.rst:33 +#: ../../whats_new.rst:66 msgid "Add :attr:`MessageFlags.voice`" msgstr ":attr:`MessageFlags.voice` を追加しました。" -#: ../../whats_new.rst:34 +#: ../../whats_new.rst:67 msgid "Add :attr:`Attachment.duration` and :attr:`Attachment.waveform`" msgstr ":attr:`Attachment.duration` と :attr:`Attachment.waveform` を追加しました。" -#: ../../whats_new.rst:35 +#: ../../whats_new.rst:68 msgid "Add :meth:`Attachment.is_voice_message`" msgstr ":meth:`Attachment.is_voice_message` を追加しました。" -#: ../../whats_new.rst:36 +#: ../../whats_new.rst:69 msgid "This does not support *sending* voice messages because this is currently unsupported by the API." msgstr "ボイスメッセージの *送信* は現在APIで対応していないため、サポートされていません。" -#: ../../whats_new.rst:38 +#: ../../whats_new.rst:71 msgid "Add support for new :attr:`Interaction.channel` attribute from the API update (:issue:`9339`)." msgstr "API更新で追加された :attr:`Interaction.channel` 属性のサポートを追加しました。 (:issue:`9339`)" -#: ../../whats_new.rst:39 +#: ../../whats_new.rst:72 msgid "Add support for :attr:`TextChannel.default_thread_slowmode_delay` (:issue:`9291`)." msgstr ":attr:`TextChannel.default_thread_slowmode_delay` のサポートを追加しました。 (:issue:`9291`)" -#: ../../whats_new.rst:40 +#: ../../whats_new.rst:73 msgid "Add support for :attr:`ForumChannel.default_sort_order` (:issue:`9290`)." msgstr ":attr:`ForumChannel.default_sort_order` のサポートを追加しました。 (:issue:`9290`)" -#: ../../whats_new.rst:41 +#: ../../whats_new.rst:74 msgid "Add support for ``default_reaction_emoji`` and ``default_forum_layout`` in :meth:`Guild.create_forum` (:issue:`9300`)." msgstr ":meth:`Guild.create_forum` にて、 ``default_reaction_emoji`` と ``default_forum_layout`` のサポートを追加しました。 (:issue:`9300`)" -#: ../../whats_new.rst:42 +#: ../../whats_new.rst:75 msgid "Add support for ``widget_channel``, ``widget_enabled``, and ``mfa_level`` in :meth:`Guild.edit` (:issue:`9302`, :issue:`9303`)." msgstr ":meth:`Guild.edit` にて、 ``widget_channel`` 、 ``widget_enabled`` 、 ``mfa_level`` のサポートを追加しました。(:issue:`9302` 、 :issue:`9303`)" -#: ../../whats_new.rst:45 +#: ../../whats_new.rst:78 msgid "Add various new :class:`Permissions` and changes (:issue:`9312`, :issue:`9325`, :issue:`9358`, :issue:`9378`)" msgstr "新しい :class:`Permissions` を追加しました。 (:issue:`9312` 、 :issue:`9325` 、 :issue:`9358` 、 :issue:`9378`)" -#: ../../whats_new.rst:44 +#: ../../whats_new.rst:77 msgid "Add new :attr:`~Permissions.manage_expressions`, :attr:`~Permissions.use_external_sounds`, :attr:`~Permissions.use_soundboard`, :attr:`~Permissions.send_voice_messages`, :attr:`~Permissions.create_expressions` permissions." msgstr "新しい :attr:`~Permissions.manage_expressions` 、 :attr:`~Permissions.use_external_sounds` 、 :attr:`~Permissions.use_soundboard` 、 :attr:`~Permissions.send_voice_messages` 、 :attr:`~Permissions.create_expressions` 権限を追加しました。" -#: ../../whats_new.rst:45 +#: ../../whats_new.rst:78 msgid "Change :attr:`Permissions.manage_emojis` to be an alias of :attr:`~Permissions.manage_expressions`." msgstr ":attr:`Permissions.manage_emojis` を :attr:`~Permissions.manage_expressions` のエイリアスに変更しました。" -#: ../../whats_new.rst:47 +#: ../../whats_new.rst:80 msgid "Add various new properties to :class:`PartialAppInfo` and :class:`AppInfo` (:issue:`9298`)." msgstr ":class:`PartialAppInfo` と :class:`AppInfo` にさまざまな新しいプロパティを追加しました。 (:issue:`9298`)" -#: ../../whats_new.rst:48 +#: ../../whats_new.rst:81 msgid "Add support for ``with_counts`` parameter to :meth:`Client.fetch_guilds` (:issue:`9369`)." msgstr ":meth:`Client.fetch_guilds` に ``with_counts`` 引数のサポートを追加しました。 (:issue:`9369`)" -#: ../../whats_new.rst:49 +#: ../../whats_new.rst:82 msgid "Add new :meth:`Guild.get_emoji` helper (:issue:`9296`)." msgstr "新しく :meth:`Guild.get_emoji` ヘルパーを追加しました。 (:issue:`9296`)" -#: ../../whats_new.rst:50 +#: ../../whats_new.rst:83 msgid "Add :attr:`ApplicationFlags.auto_mod_badge` (:issue:`9313`)." msgstr ":attr:`ApplicationFlags.auto_mod_badge` を追加しました。 (:issue:`9313`)" -#: ../../whats_new.rst:51 +#: ../../whats_new.rst:84 msgid "Add :attr:`Guild.max_stage_video_users` and :attr:`Guild.safety_alerts_channel` (:issue:`9318`)." msgstr ":attr:`Guild.max_stage_video_users` と :attr:`Guild.safety_alerts_channel` を追加しました。 (:issue:`9318`)" -#: ../../whats_new.rst:52 +#: ../../whats_new.rst:85 msgid "Add support for ``raid_alerts_disabled`` and ``safety_alerts_channel`` in :meth:`Guild.edit` (:issue:`9318`)." msgstr ":meth:`Guild.edit` にて ``raid_alerts_disabled`` と ``safety_alerts_channel`` のサポートを追加しました。 (:issue:`9318`)" -#: ../../whats_new.rst:53 +#: ../../whats_new.rst:86 msgid "|commands| Add :attr:`BadLiteralArgument.argument ` to get the failed argument's value (:issue:`9283`)." msgstr "|commands| 失敗した引数の値を取得するための :attr:`BadLiteralArgument.argument ` を追加しました。 (:issue:`9283`)" -#: ../../whats_new.rst:54 +#: ../../whats_new.rst:87 msgid "|commands| Add :attr:`Context.filesize_limit ` property (:issue:`9416`)." msgstr "|commands| :attr:`Context.filesize_limit ` 属性を追加しました。 (:issue:`9416`)" -#: ../../whats_new.rst:55 +#: ../../whats_new.rst:88 msgid "|commands| Add support for :attr:`Parameter.displayed_name ` (:issue:`9427`)." msgstr "|commands| :attr:`Parameter.displayed_name ` のサポートを追加しました。 (:issue:`9427`)" -#: ../../whats_new.rst:58 -#: ../../whats_new.rst:92 -#: ../../whats_new.rst:106 -#: ../../whats_new.rst:159 -#: ../../whats_new.rst:196 -msgid "Bug Fixes" -msgstr "バグ修正" - -#: ../../whats_new.rst:61 +#: ../../whats_new.rst:94 msgid "Fix ``FileHandler`` handlers being written ANSI characters when the bot is executed inside PyCharm." msgstr "PyCharm 内でボットが実行された場合、 ``FileHandler`` ハンドラにANSI 文字が出力されるのを修正しました。" -#: ../../whats_new.rst:61 +#: ../../whats_new.rst:94 msgid "This has the side effect of removing coloured logs from the PyCharm terminal due an upstream bug involving TTY detection. This issue is tracked under `PY-43798 `_." msgstr "PyCharmのTTY検出のバグの影響により、PyCharm ターミナル内でログに色が付かなくなる副作用があります。このバグは `PY-43798 `_ で追跡されています。" -#: ../../whats_new.rst:63 +#: ../../whats_new.rst:96 msgid "Fix channel edits with :meth:`Webhook.edit` sending two requests instead of one." msgstr ":meth:`Webhook.edit` でチャンネルを編集するときに2回リクエストが行われるバグを修正しました。" -#: ../../whats_new.rst:64 +#: ../../whats_new.rst:97 msgid "Fix :attr:`StageChannel.last_message_id` always being ``None`` (:issue:`9422`)." msgstr ":attr:`StageChannel.last_message_id` が常に ``None`` となるのを修正しました。 (:issue:`9422`)" -#: ../../whats_new.rst:65 +#: ../../whats_new.rst:98 msgid "Fix piped audio input ending prematurely (:issue:`9001`, :issue:`9380`)." msgstr "パイプによるオーディオ入力が終了するのが早すぎる問題を修正しました。 (:issue:`9001` 、 :issue:`9380`)" -#: ../../whats_new.rst:66 +#: ../../whats_new.rst:99 msgid "Fix persistent detection for :class:`ui.TextInput` being incorrect if the ``custom_id`` is set later (:issue:`9438`)." msgstr "``custom_id`` が後で設定された場合、 :class:`ui.TextInput` の永続的な検出が正しくない問題を修正しました。 (:issue:`9438`)" -#: ../../whats_new.rst:67 +#: ../../whats_new.rst:100 msgid "Fix custom attributes not being copied over when inheriting from :class:`app_commands.Group` (:issue:`9383`)." msgstr ":class:`app_commands.Group` から継承するときにカスタム属性がコピーされない問題を修正しました。 (:issue:`9383`)" -#: ../../whats_new.rst:68 +#: ../../whats_new.rst:101 msgid "Fix AutoMod audit log entry error due to empty channel_id (:issue:`9384`)." msgstr "空の channel_id により自動管理の監査ログ項目でエラーが発生するのを修正しました。 (:issue:`9384`)" -#: ../../whats_new.rst:69 +#: ../../whats_new.rst:102 msgid "Fix handling of ``around`` parameter in :meth:`abc.Messageable.history` (:issue:`9388`)." msgstr ":meth:`abc.Messageable.history` の ``around`` 引数の扱いを修正しました。 (:issue:`9388`)" -#: ../../whats_new.rst:70 +#: ../../whats_new.rst:103 msgid "Fix occasional :exc:`AttributeError` when accessing the :attr:`ClientUser.mutual_guilds` property (:issue:`9387`)." msgstr ":attr:`ClientUser.mutual_guilds` プロパティにアクセスするとき時々 :exc:`AttributeError` が発生する問題を修正しました。 (:issue:`9387`)" -#: ../../whats_new.rst:71 +#: ../../whats_new.rst:104 msgid "Fix :func:`utils.escape_markdown` not escaping the new markdown (:issue:`9361`)." msgstr ":func:`utils.escape_markdown` が新しいマークダウンを正しくエスケープしない問題を修正しました。 (:issue:`9361`)" -#: ../../whats_new.rst:72 +#: ../../whats_new.rst:105 msgid "Fix webhook targets not being converted in audit logs (:issue:`9332`)." msgstr "監査ログでWebhookターゲットが変換されない問題を修正しました。 (:issue:`9332`)" -#: ../../whats_new.rst:73 +#: ../../whats_new.rst:106 msgid "Fix error when not passing ``enabled`` in :meth:`Guild.create_automod_rule` (:issue:`9292`)." msgstr ":meth:`Guild.create_automod_rule` で ``enabled`` を渡さないときに生じるエラーを修正しました。 (:issue:`9292`)" -#: ../../whats_new.rst:74 +#: ../../whats_new.rst:107 msgid "Fix how various parameters are handled in :meth:`Guild.create_scheduled_event` (:issue:`9275`)." msgstr ":meth:`Guild.create_scheduled_event` のパラメータの扱いを修正しました。 (:issue:`9275`)" -#: ../../whats_new.rst:75 +#: ../../whats_new.rst:108 msgid "Fix not sending the ``ssrc`` parameter when sending the SPEAKING payload (:issue:`9301`)." msgstr "SPEAKING ペイロードの送信時に ``ssrc`` パラメータを送信しない問題を修正しました。 (:issue:`9301`)" -#: ../../whats_new.rst:76 +#: ../../whats_new.rst:109 msgid "Fix :attr:`Message.guild` being ``None`` sometimes when received via an interaction." msgstr "インタラクションで受け取った :attr:`Message.guild` が時々 ``None`` になる問題を修正しました。" -#: ../../whats_new.rst:77 +#: ../../whats_new.rst:110 msgid "Fix :attr:`Message.system_content` for :attr:`MessageType.channel_icon_change` (:issue:`9410`)." msgstr ":attr:`MessageType.channel_icon_change` の :attr:`Message.system_content` を修正しました。 (:issue:`9410`)" -#: ../../whats_new.rst:80 -#: ../../whats_new.rst:180 -#: ../../whats_new.rst:250 -#: ../../whats_new.rst:392 -#: ../../whats_new.rst:459 +#: ../../whats_new.rst:113 +#: ../../whats_new.rst:213 +#: ../../whats_new.rst:283 +#: ../../whats_new.rst:425 +#: ../../whats_new.rst:492 msgid "Miscellaneous" msgstr "その他" -#: ../../whats_new.rst:82 +#: ../../whats_new.rst:115 msgid "Update the base :attr:`Guild.filesize_limit` to 25MiB (:issue:`9353`)." msgstr "基本の :attr:`Guild.filesize_limit` を 25MiB に更新しました。 (:issue:`9353`)" -#: ../../whats_new.rst:83 +#: ../../whats_new.rst:116 msgid "Allow Interaction webhook URLs to be used in :meth:`Webhook.from_url`." msgstr ":meth:`Webhook.from_url` でインタラクション Webhook URLを使用できるようになりました。" -#: ../../whats_new.rst:84 +#: ../../whats_new.rst:117 msgid "Set the socket family of internal connector to ``AF_INET`` to prevent IPv6 connections (:issue:`9442`, :issue:`9443`)." msgstr "IPv6 接続を防ぐために、内部コネクタのソケットファミリを ``AF_INET`` に設定するようにしました。 (:issue:`9442` 、 :issue:`9443`)" -#: ../../whats_new.rst:89 +#: ../../whats_new.rst:122 msgid "v2.2.3" msgstr "v2.2.3" -#: ../../whats_new.rst:94 +#: ../../whats_new.rst:127 msgid "Fix crash from Discord sending null ``channel_id`` for automod audit logs." msgstr "Discordが自動管理の監査ログに関し null の ``channel_id`` を送ることによって生じたクラッシュを修正しました。" -#: ../../whats_new.rst:95 +#: ../../whats_new.rst:128 msgid "Fix ``channel`` edits when using :meth:`Webhook.edit` sending two requests." msgstr ":meth:`Webhook.edit` を使用して ``channel`` を変更するときに2回リクエストが送信されるバグを修正しました。" -#: ../../whats_new.rst:96 +#: ../../whats_new.rst:129 msgid "Fix :attr:`AuditLogEntry.target` being ``None`` for invites (:issue:`9336`)." msgstr "招待に関し :attr:`AuditLogEntry.target` が ``None`` となるのを修正しました。 (:issue:`9336`)" -#: ../../whats_new.rst:97 +#: ../../whats_new.rst:130 msgid "Fix :exc:`KeyError` when accessing data for :class:`GuildSticker` (:issue:`9324`)." msgstr ":class:`GuildSticker` のデータにアクセスするときの :exc:`KeyError` を修正しました。 (:issue:`9324`)" -#: ../../whats_new.rst:103 +#: ../../whats_new.rst:136 msgid "v2.2.2" msgstr "v2.2.2" -#: ../../whats_new.rst:108 +#: ../../whats_new.rst:141 msgid "Fix UDP discovery in voice not using new 74 byte layout which caused voice to break (:issue:`9277`, :issue:`9278`)" msgstr "ボイスのUDP検出が、新しい74バイトレイアウトを使用していないため、ボイスが使用できない問題を修正しました。 (:issue:`9277` 、 :issue:`9278`)" -#: ../../whats_new.rst:113 +#: ../../whats_new.rst:146 msgid "v2.2.0" msgstr "v2.2.0" -#: ../../whats_new.rst:118 +#: ../../whats_new.rst:151 msgid "Add support for new :func:`on_audit_log_entry_create` event" msgstr "新しい :func:`on_audit_log_entry_create` イベントのサポートを追加しました。" -#: ../../whats_new.rst:120 +#: ../../whats_new.rst:153 msgid "Add support for silent messages via ``silent`` parameter in :meth:`abc.Messageable.send`" msgstr "サイレントメッセージを送信する :meth:`abc.Messageable.send` の ``silent`` パラメータのサポートを追加しました。" -#: ../../whats_new.rst:120 +#: ../../whats_new.rst:153 msgid "This is queryable via :attr:`MessageFlags.suppress_notifications`" msgstr "これは :attr:`MessageFlags.suppress_notifications` から確認できます。" -#: ../../whats_new.rst:122 +#: ../../whats_new.rst:155 msgid "Implement :class:`abc.Messageable` for :class:`StageChannel` (:issue:`9248`)" msgstr ":class:`StageChannel` が :class:`abc.Messageable` を実装するようにしました。 (:issue:`9248`)" -#: ../../whats_new.rst:123 +#: ../../whats_new.rst:156 msgid "Add setter for :attr:`discord.ui.ChannelSelect.channel_types` (:issue:`9068`)" msgstr ":attr:`discord.ui.ChannelSelect.channel_types` のセッターを追加しました。 (:issue:`9068`)" -#: ../../whats_new.rst:124 +#: ../../whats_new.rst:157 msgid "Add support for custom messages in automod via :attr:`AutoModRuleAction.custom_message` (:issue:`9267`)" msgstr ":attr:`AutoModRuleAction.custom_message` で、自動管理のカスタムメッセージのサポートを追加しました。 (:issue:`9267`)" -#: ../../whats_new.rst:125 +#: ../../whats_new.rst:158 msgid "Add :meth:`ForumChannel.get_thread` (:issue:`9106`)" msgstr ":meth:`ForumChannel.get_thread` を追加しました。 (:issue:`9106`)" -#: ../../whats_new.rst:126 +#: ../../whats_new.rst:159 msgid "Add :attr:`StageChannel.slowmode_delay` and :attr:`VoiceChannel.slowmode_delay` (:issue:`9111`)" msgstr ":attr:`StageChannel.slowmode_delay` と :attr:`VoiceChannel.slowmode_delay` を追加しました。 (:issue:`9111`)" -#: ../../whats_new.rst:127 +#: ../../whats_new.rst:160 msgid "Add support for editing the slowmode for :class:`StageChannel` and :class:`VoiceChannel` (:issue:`9111`)" msgstr ":class:`StageChannel` と :class:`VoiceChannel` の低速モードの変更のサポートを追加しました。 (:issue:`9111`)" -#: ../../whats_new.rst:128 +#: ../../whats_new.rst:161 msgid "Add :attr:`Locale.indonesian`" msgstr ":attr:`Locale.indonesian` を追加しました。" -#: ../../whats_new.rst:129 +#: ../../whats_new.rst:162 msgid "Add ``delete_after`` keyword argument to :meth:`Interaction.edit_message` (:issue:`9415`)" msgstr ":meth:`Interaction.edit_message` に ``delete_after`` キーワード引数を追加しました。 (:issue:`9415`)" -#: ../../whats_new.rst:130 +#: ../../whats_new.rst:163 msgid "Add ``delete_after`` keyword argument to :meth:`InteractionMessage.edit` (:issue:`9206`)" msgstr ":meth:`InteractionMessage.edit` に ``delete_after`` キーワード引数を追加しました。 (:issue:`9206`)" -#: ../../whats_new.rst:133 +#: ../../whats_new.rst:166 msgid "Add support for member flags (:issue:`9204`)" msgstr "メンバーフラグのサポートを追加しました。 (:issue:`9204`)" -#: ../../whats_new.rst:132 +#: ../../whats_new.rst:165 msgid "Accessible via :attr:`Member.flags` and has a type of :class:`MemberFlags`" msgstr ":attr:`Member.flags` でアクセスでき、型は :class:`MemberFlags` です。" -#: ../../whats_new.rst:133 +#: ../../whats_new.rst:166 msgid "Support ``bypass_verification`` within :meth:`Member.edit`" msgstr ":meth:`Member.edit` にて ``bypass_verification`` のサポートを追加しました。" -#: ../../whats_new.rst:136 +#: ../../whats_new.rst:169 msgid "Add support for passing a client to :meth:`Webhook.from_url` and :meth:`Webhook.partial`" msgstr ":meth:`Webhook.from_url` と :meth:`Webhook.partial` にクライアントを渡せるようにしました。" -#: ../../whats_new.rst:136 +#: ../../whats_new.rst:169 msgid "This allows them to use views (assuming they are \"bot owned\" webhooks)" msgstr "これにより、ビューを使用することができます (「ボット所有」Webhookである場合は)。" -#: ../../whats_new.rst:138 +#: ../../whats_new.rst:171 msgid "Add :meth:`Colour.dark_embed` and :meth:`Colour.light_embed` (:issue:`9219`)" msgstr ":meth:`Colour.dark_embed` と :meth:`Colour.light_embed` を追加しました。 (:issue:`9219`)" -#: ../../whats_new.rst:139 +#: ../../whats_new.rst:172 msgid "Add support for many more parameters within :meth:`Guild.create_stage_channel` (:issue:`9245`)" msgstr ":meth:`Guild.create_stage_channel` で対応するパラメータを追加しました。 (:issue:`9245`)" -#: ../../whats_new.rst:140 +#: ../../whats_new.rst:173 msgid "Add :attr:`AppInfo.role_connections_verification_url`" msgstr ":attr:`AppInfo.role_connections_verification_url` を追加しました。" -#: ../../whats_new.rst:141 +#: ../../whats_new.rst:174 msgid "Add support for :attr:`ForumChannel.default_layout`" msgstr ":attr:`ForumChannel.default_layout` のサポートを追加しました。" -#: ../../whats_new.rst:142 +#: ../../whats_new.rst:175 msgid "Add various new :class:`MessageType` values such as ones related to stage channel and role subscriptions" msgstr "ステージチャンネルやロールサブスクリプションに関連するものなど、新しい :class:`MessageType` 値を追加しました。" -#: ../../whats_new.rst:149 +#: ../../whats_new.rst:182 msgid "Add support for role subscription related attributes" msgstr "ロールサブスクリプション関連属性のサポートを追加しました。" -#: ../../whats_new.rst:144 +#: ../../whats_new.rst:177 msgid ":class:`RoleSubscriptionInfo` within :attr:`Message.role_subscription`" msgstr ":attr:`Message.role_subscription` と :class:`RoleSubscriptionInfo` 。" -#: ../../whats_new.rst:145 +#: ../../whats_new.rst:178 msgid ":attr:`MessageType.role_subscription_purchase`" msgstr ":attr:`MessageType.role_subscription_purchase`" -#: ../../whats_new.rst:146 +#: ../../whats_new.rst:179 msgid ":attr:`SystemChannelFlags.role_subscription_purchase_notifications`" msgstr ":attr:`SystemChannelFlags.role_subscription_purchase_notifications`" -#: ../../whats_new.rst:147 +#: ../../whats_new.rst:180 msgid ":attr:`SystemChannelFlags.role_subscription_purchase_notification_replies`" msgstr ":attr:`SystemChannelFlags.role_subscription_purchase_notification_replies`" -#: ../../whats_new.rst:148 +#: ../../whats_new.rst:181 msgid ":attr:`RoleTags.subscription_listing_id`" msgstr ":attr:`RoleTags.subscription_listing_id`" -#: ../../whats_new.rst:149 +#: ../../whats_new.rst:182 msgid ":meth:`RoleTags.is_available_for_purchase`" msgstr ":meth:`RoleTags.is_available_for_purchase`" -#: ../../whats_new.rst:151 +#: ../../whats_new.rst:184 msgid "Add support for checking if a role is a linked role under :meth:`RoleTags.is_guild_connection`" msgstr ":meth:`RoleTags.is_guild_connection` で、ロールが紐づいたロールかの確認のサポートを追加しました。" -#: ../../whats_new.rst:152 +#: ../../whats_new.rst:185 msgid "Add support for GIF sticker type" msgstr "GIFスタンプタイプのサポートを追加しました。" -#: ../../whats_new.rst:153 +#: ../../whats_new.rst:186 msgid "Add support for :attr:`Message.application_id` and :attr:`Message.position`" msgstr ":attr:`Message.application_id` と :attr:`Message.position` のサポートを追加しました。" -#: ../../whats_new.rst:154 +#: ../../whats_new.rst:187 msgid "Add :func:`utils.maybe_coroutine` helper" msgstr ":func:`utils.maybe_coroutine` ヘルパーを追加しました。" -#: ../../whats_new.rst:155 +#: ../../whats_new.rst:188 msgid "Add :attr:`ScheduledEvent.creator_id` attribute" msgstr ":attr:`ScheduledEvent.creator_id` 属性を追加しました。" -#: ../../whats_new.rst:156 +#: ../../whats_new.rst:189 msgid "|commands| Add support for :meth:`~ext.commands.Cog.interaction_check` for :class:`~ext.commands.GroupCog` (:issue:`9189`)" msgstr "|commands| :class:`~ext.commands.GroupCog` にて :meth:`~ext.commands.Cog.interaction_check` のサポートを追加しました。 (:issue:`9189`)" -#: ../../whats_new.rst:161 +#: ../../whats_new.rst:194 msgid "Fix views not being removed from message store backing leading to a memory leak when used from an application command context" msgstr "アプリケーションコマンドから使用されたビューがメッセージストアから除去されず、メモリリークを引き起こすバグを修正しました。" -#: ../../whats_new.rst:162 +#: ../../whats_new.rst:195 msgid "Fix async iterators requesting past their bounds when using ``oldest_first`` and ``after`` or ``before`` (:issue:`9093`)" msgstr "非同期イテレータが ``oldest_first`` と ``after`` または ``before`` を指定した場合に境界を越えてリクエストをするのを修正しました。 (:issue:`9093`)" -#: ../../whats_new.rst:163 +#: ../../whats_new.rst:196 msgid "Fix :meth:`Guild.audit_logs` pagination logic being buggy when using ``after`` (:issue:`9269`)" msgstr ":meth:`Guild.audit_logs` にて、 ``after`` を使用したときにページネーションで発生するバグを修正しました。 (:issue:`9269`)" -#: ../../whats_new.rst:164 +#: ../../whats_new.rst:197 msgid "Fix :attr:`Message.channel` sometimes being :class:`Object` instead of :class:`PartialMessageable`" msgstr ":attr:`Message.channel` が時々 :class:`PartialMessageable` ではなく :class:`Object` となるバグを修正しました。" -#: ../../whats_new.rst:165 +#: ../../whats_new.rst:198 msgid "Fix :class:`ui.View` not properly calling ``super().__init_subclass__`` (:issue:`9231`)" msgstr ":class:`ui.View` が ``super().__init_subclass__`` を適切に呼び出さないのを修正しました。 (:issue:`9231`)" -#: ../../whats_new.rst:166 +#: ../../whats_new.rst:199 msgid "Fix ``available_tags`` and ``default_thread_slowmode_delay`` not being respected in :meth:`Guild.create_forum`" msgstr ":meth:`Guild.create_forum` で渡された ``available_tags`` と ``default_thread_slowmode_delay`` が使用されない問題を修正しました。" -#: ../../whats_new.rst:167 +#: ../../whats_new.rst:200 msgid "Fix :class:`AutoModTrigger` ignoring ``allow_list`` with type keyword (:issue:`9107`)" msgstr ":class:`AutoModTrigger` が type キーワードのある ``allow_list`` を無視するバグを修正しました。 (:issue:`9107`)" -#: ../../whats_new.rst:168 +#: ../../whats_new.rst:201 msgid "Fix implicit permission resolution for :class:`Thread` (:issue:`9153`)" msgstr ":class:`Thread` の暗黙的な権限の解決を修正しました。 (:issue:`9153`)" -#: ../../whats_new.rst:169 +#: ../../whats_new.rst:202 msgid "Fix :meth:`AutoModRule.edit` to work with actual snowflake types such as :class:`Object` (:issue:`9159`)" msgstr ":meth:`AutoModRule.edit` を、 :class:`Object` のようなスノウフレーク型で動くよう修正しました。 (:issue:`9159`)" -#: ../../whats_new.rst:170 +#: ../../whats_new.rst:203 msgid "Fix :meth:`Webhook.send` returning :class:`ForumChannel` for :attr:`WebhookMessage.channel`" msgstr ":meth:`Webhook.send` が :attr:`WebhookMessage.channel` に関し :class:`ForumChannel` を返すのを修正しました。" -#: ../../whats_new.rst:171 +#: ../../whats_new.rst:204 msgid "When a lookup for :attr:`AuditLogEntry.target` fails, it will fallback to :class:`Object` with the appropriate :attr:`Object.type` (:issue:`9171`)" msgstr ":attr:`AuditLogEntry.target` の検索が失敗したとき、適切な :attr:`Object.type` をもつ :class:`Object` にフォールバックするようにしました。 (:issue:`9171`)" -#: ../../whats_new.rst:172 +#: ../../whats_new.rst:205 msgid "Fix :attr:`AuditLogDiff.type` for integrations returning :class:`ChannelType` instead of :class:`str` (:issue:`9200`)" msgstr "インテグレーションの :attr:`AuditLogDiff.type` が :class:`str` ではなく :class:`ChannelType` を返すのを修正しました。 (:issue:`9200`)" -#: ../../whats_new.rst:173 +#: ../../whats_new.rst:206 msgid "Fix :attr:`AuditLogDiff.type` for webhooks returning :class:`ChannelType` instead of :class:`WebhookType` (:issue:`9251`)" msgstr "Webhookの :attr:`AuditLogDiff.type` が :class:`WebhookType` ではなく :class:`ChannelType` を返すのを修正しました。 (:issue:`9251`)" -#: ../../whats_new.rst:174 +#: ../../whats_new.rst:207 msgid "Fix webhooks and interactions not properly closing files after the request has completed" msgstr "Webhookとインタラクションが、リクエストが完了した後にファイルを正しく閉じないバグを修正しました。" -#: ../../whats_new.rst:175 +#: ../../whats_new.rst:208 msgid "Fix :exc:`NameError` in audit log target for app commands" msgstr "アプリケーションコマンドの監査ログターゲットでの :exc:`NameError` を修正しました。" -#: ../../whats_new.rst:176 +#: ../../whats_new.rst:209 msgid "Fix :meth:`ScheduledEvent.edit` requiring some arguments to be passed in when unnecessary (:issue:`9261`, :issue:`9268`)" msgstr ":meth:`ScheduledEvent.edit` にて不必要な引数が必須とされるバグを修正しました。 (:issue:`9261` 、 :issue:`9268`)" -#: ../../whats_new.rst:177 +#: ../../whats_new.rst:210 msgid "|commands| Explicit set a traceback for hybrid command invocations (:issue:`9205`)" msgstr "|commands| ハイブリッドコマンドを呼び出すとき、明示的にトレースバックを設定するようにしました。 (:issue:`9205`)" -#: ../../whats_new.rst:182 +#: ../../whats_new.rst:215 msgid "Add colour preview for the colours predefined in :class:`Colour`" msgstr ":class:`Colour` で定義された色のプレビューを追加しました。" -#: ../../whats_new.rst:183 +#: ../../whats_new.rst:216 msgid "Finished views are no longer stored by the library when sending them (:issue:`9235`)" msgstr "終了したビューは送信時にライブラリで保管されないようになりました。 (:issue:`9235`)" -#: ../../whats_new.rst:184 +#: ../../whats_new.rst:217 msgid "Force enable colour logging for the default logging handler when run under Docker." msgstr "Docker下で実行するときに、デフォルトの logging ハンドラで色のついたログを常に有効にするようにしました。" -#: ../../whats_new.rst:185 +#: ../../whats_new.rst:218 msgid "Add various overloads for :meth:`Client.wait_for` to aid in static analysis (:issue:`9184`)" msgstr "静的解析のために、 :meth:`Client.wait_for` のオーバーロードを追加しました。 (:issue:`9184`)" -#: ../../whats_new.rst:186 +#: ../../whats_new.rst:219 msgid ":class:`Interaction` can now optionally take a generic parameter, ``ClientT`` to represent the type for :attr:`Interaction.client`" msgstr ":class:`Interaction` は、オプションでジェネリックのパラメータ ``ClientT`` をとり、 :attr:`Interaction.client` の型を指定できるようになりました。" -#: ../../whats_new.rst:187 +#: ../../whats_new.rst:220 msgid "|commands| Respect :attr:`~ext.commands.Command.ignore_extra` for :class:`~discord.ext.commands.FlagConverter` keyword-only parameters" msgstr "|commands| :class:`~discord.ext.commands.FlagConverter` キーワードのみのパラメータでも、 :attr:`~ext.commands.Command.ignore_extra` に従うようにしました。" -#: ../../whats_new.rst:188 +#: ../../whats_new.rst:221 msgid "|commands| Change :attr:`Paginator.pages ` to not prematurely close (:issue:`9257`)" msgstr "|commands| :attr:`Paginator.pages ` を早期に閉じないように変更しました。 (:issue:`9257`)" -#: ../../whats_new.rst:193 +#: ../../whats_new.rst:226 msgid "v2.1.1" msgstr "v2.1.1" -#: ../../whats_new.rst:198 +#: ../../whats_new.rst:231 msgid "Fix crash involving GIF stickers when looking up their filename extension." msgstr "GIF スタンプのファイル名の拡張子を検索するときのクラッシュを修正しました。" -#: ../../whats_new.rst:203 +#: ../../whats_new.rst:236 msgid "v2.1.0" msgstr "v2.1.0" -#: ../../whats_new.rst:208 +#: ../../whats_new.rst:241 msgid "Add support for ``delete_message_seconds`` in :meth:`Guild.ban` (:issue:`8391`)" msgstr ":meth:`Guild.ban` に ``delete_message_seconds`` へのサポートを追加しました。 (:issue:`8391`)" -#: ../../whats_new.rst:209 +#: ../../whats_new.rst:242 msgid "Add support for automod related audit log actions (:issue:`8389`)" msgstr "AutoMod関連の監査ログアクションのサポートを追加しました。 (:issue:`8389`)" -#: ../../whats_new.rst:210 +#: ../../whats_new.rst:243 msgid "Add support for :class:`ForumChannel` annotations in app commands" msgstr "アプリケーションコマンドで :class:`ForumChannel` アノテーションのサポートを追加しました。" -#: ../../whats_new.rst:211 +#: ../../whats_new.rst:244 msgid "Add support for :attr:`ForumChannel.default_thread_slowmode_delay`." msgstr ":attr:`ForumChannel.default_thread_slowmode_delay` のサポートを追加しました。" -#: ../../whats_new.rst:212 +#: ../../whats_new.rst:245 msgid "Add support for :attr:`ForumChannel.default_reaction_emoji`." msgstr ":attr:`ForumChannel.default_reaction_emoji` のサポートを追加しました。" -#: ../../whats_new.rst:215 +#: ../../whats_new.rst:248 msgid "Add support for forum tags under :class:`ForumTag`." msgstr ":class:`ForumTag` にて、フォーラムタグのサポートを追加しました。" -#: ../../whats_new.rst:214 +#: ../../whats_new.rst:247 msgid "Tags can be obtained using :attr:`ForumChannel.available_tags` or :meth:`ForumChannel.get_tag`." msgstr "タグは :attr:`ForumChannel.available_tags` または :meth:`ForumChannel.get_tag` で取得できます。" -#: ../../whats_new.rst:215 +#: ../../whats_new.rst:248 msgid "See :meth:`Thread.edit` and :meth:`ForumChannel.edit` for modifying tags and their usage." msgstr "タグの変更方法や使い方については :meth:`Thread.edit` と :meth:`ForumChannel.edit` を参照してください。" -#: ../../whats_new.rst:219 +#: ../../whats_new.rst:252 msgid "Add support for new select types (:issue:`9013`, :issue:`9003`)." msgstr "新しい選択メニューの種類のサポートを追加しました。 (:issue:`9013`, :issue:`9003`)" -#: ../../whats_new.rst:218 +#: ../../whats_new.rst:251 msgid "These are split into separate classes, :class:`~discord.ui.ChannelSelect`, :class:`~discord.ui.RoleSelect`, :class:`~discord.ui.UserSelect`, :class:`~discord.ui.MentionableSelect`." msgstr "これらは、 :class:`~discord.ui.ChannelSelect` 、 :class:`~discord.ui.RoleSelect` 、 :class:`~discord.ui.UserSelect` 、 :class:`~discord.ui.MentionableSelect` に分割されています。" -#: ../../whats_new.rst:219 +#: ../../whats_new.rst:252 msgid "The decorator still uses a single function, :meth:`~discord.ui.select`. Changing the select type is done by the ``cls`` keyword parameter." msgstr "デコレータはこれまで通り単一の関数 :meth:`~discord.ui.select` を使用しています。選択メニューの種類の変更は ``cls`` キーワード引数によって行われます。" -#: ../../whats_new.rst:221 +#: ../../whats_new.rst:254 msgid "Add support for toggling discoverable and invites_disabled features in :meth:`Guild.edit` (:issue:`8390`)." msgstr ":meth:`Guild.edit` で、discoverable と invites_disabled 機能を切り替えるためのサポートを追加しました。 (:issue:`8390`)" -#: ../../whats_new.rst:222 +#: ../../whats_new.rst:255 msgid "Add :meth:`Interaction.translate` helper method (:issue:`8425`)." msgstr ":meth:`Interaction.translate` ヘルパーメソッドを追加しました。 (:issue:`8425`)" -#: ../../whats_new.rst:223 +#: ../../whats_new.rst:256 msgid "Add :meth:`Forum.archived_threads` (:issue:`8476`)." msgstr ":meth:`Forum.archived_threads` を追加しました。 (:issue:`8476`)" -#: ../../whats_new.rst:224 +#: ../../whats_new.rst:257 msgid "Add :attr:`ApplicationFlags.active`, :attr:`UserFlags.active_developer`, and :attr:`PublicUserFlags.active_developer`." msgstr ":attr:`ApplicationFlags.active` 、 :attr:`UserFlags.active_developer` 、および :attr:`PublicUserFlags.active_developer` を追加しました。" -#: ../../whats_new.rst:225 +#: ../../whats_new.rst:258 msgid "Add ``delete_after`` to :meth:`InteractionResponse.send_message` (:issue:`9022`)." msgstr ":meth:`InteractionResponse.send_message` に ``delete_after`` を追加しました。 (:issue:`9022`)" -#: ../../whats_new.rst:226 +#: ../../whats_new.rst:259 msgid "Add support for :attr:`AutoModTrigger.regex_patterns`." msgstr ":attr:`AutoModTrigger.regex_patterns` のサポートを追加しました。" -#: ../../whats_new.rst:227 +#: ../../whats_new.rst:260 msgid "|commands| Add :attr:`GroupCog.group_extras ` to set :attr:`app_commands.Group.extras` (:issue:`8405`)." msgstr "|commands| :attr:`app_commands.Group.extras` を設定できる :attr:`GroupCog.group_extras ` を追加しました。 (:issue:`8405`)" -#: ../../whats_new.rst:228 +#: ../../whats_new.rst:261 msgid "|commands| Add support for NumPy style docstrings for regular commands to set parameter descriptions." msgstr "|commands| 通常のコマンドでパラメータの説明を設定するのに NumPy スタイルの docstring が利用できるようになりました。" -#: ../../whats_new.rst:229 +#: ../../whats_new.rst:262 msgid "|commands| Allow :class:`~discord.ext.commands.Greedy` to potentially maintain state between calls." msgstr "|commands| :class:`~discord.ext.commands.Greedy` が呼び出し間で状態を維持できるようにしました。" -#: ../../whats_new.rst:230 +#: ../../whats_new.rst:263 msgid "|commands| Add :meth:`Cog.has_app_command_error_handler ` (:issue:`8991`)." msgstr "|commands| :meth:`Cog.has_app_command_error_handler ` を追加しました。 (:issue:`8991`)" -#: ../../whats_new.rst:231 +#: ../../whats_new.rst:264 msgid "|commands| Allow ``delete_after`` in :meth:`Context.send ` on ephemeral messages (:issue:`9021`)." msgstr "|commands| :meth:`Context.send ` で ``delete_after`` が利用できるようになりました。 (:issue:`9021`)" -#: ../../whats_new.rst:236 +#: ../../whats_new.rst:269 msgid "Fix an :exc:`KeyError` being raised when constructing :class:`app_commands.Group` with no module (:issue:`8411`)." msgstr ":exc:`app_commands.Group` をモジュールなしで構築したときに :class:`KeyError` が発生する問題を修正しました。 (:issue:`8411`)" -#: ../../whats_new.rst:237 +#: ../../whats_new.rst:270 msgid "Fix unescaped period in webhook URL regex (:issue:`8443`)." msgstr "Webhook URLの正規表現でピリオドがエスケープされていない問題を修正しました。 (:issue:`8443`)" -#: ../../whats_new.rst:238 +#: ../../whats_new.rst:271 msgid "Fix :exc:`app_commands.CommandSyncFailure` raising for other 400 status code errors." msgstr "他の400ステータスコードエラーで :exc:`app_commands.CommandSyncFailure` が送出される問題を修正しました。" -#: ../../whats_new.rst:239 +#: ../../whats_new.rst:272 msgid "Fix potential formatting issues showing `_errors` in :exc:`app_commands.CommandSyncFailure`." msgstr ":exc:`app_commands.CommandSyncFailure` で ``_errors`` を表示するかもしれないフォーマットの問題を修正しました。" -#: ../../whats_new.rst:240 +#: ../../whats_new.rst:273 msgid "Fix :attr:`Guild.stage_instances` and :attr:`Guild.schedule_events` clearing on ``GUILD_UPDATE``." msgstr ":attr:`Guild.stage_instances` と :attr:`Guild.schedule_events` が ``GUILD_UPDATE`` 時に空になる問題を修正しました。" -#: ../../whats_new.rst:241 +#: ../../whats_new.rst:274 msgid "Fix detection of overriden :meth:`app_commands.Group.on_error`" msgstr "オーバーライドされた :meth:`app_commands.Group.on_error` の検出を修正しました。" -#: ../../whats_new.rst:242 +#: ../../whats_new.rst:275 msgid "Fix :meth:`app_commands.CommandTree.on_error` still being called when a bound error handler is set." msgstr "エラーハンドラが設定されている場合にも、 :meth:`app_commands.CommandTree.on_error` が呼び出されているのを修正しました。" -#: ../../whats_new.rst:243 +#: ../../whats_new.rst:276 msgid "Fix thread permissions being set to ``True`` in :meth:`DMChannel.permissions_for` (:issue:`8965`)." msgstr ":meth:`DMChannel.permissions_for` でスレッドの権限が ``True`` に設定されている問題を修正しました。 (:issue:`8965`)" -#: ../../whats_new.rst:244 +#: ../../whats_new.rst:277 msgid "Fix ``on_scheduled_event_delete`` occasionally dispatching with too many parameters (:issue:`9019`)." msgstr "``on_scheduled_event_delete`` に渡されるパラメータが多すぎる場合がある問題を修正しました。 (:issue:`9019`)" -#: ../../whats_new.rst:245 +#: ../../whats_new.rst:278 msgid "|commands| Fix :meth:`Context.from_interaction ` ignoring :attr:`~discord.ext.commands.Context.command_failed`." msgstr "|commands| :meth:`Context.from_interaction ` が :attr:`~discord.ext.commands.Context.command_failed` を無視する問題を修正しました。" -#: ../../whats_new.rst:246 +#: ../../whats_new.rst:279 msgid "|commands| Fix :class:`~discord.ext.commands.Range` to allow 3.10 Union syntax (:issue:`8446`)." msgstr "|commands| :class:`~discord.ext.commands.Range` で 3.10 Union 構文が利用できるようになりました。 (:issue:`8446`)." -#: ../../whats_new.rst:247 +#: ../../whats_new.rst:280 msgid "|commands| Fix ``before_invoke`` not triggering for fallback commands in a hybrid group command (:issue:`8461`, :issue:`8462`)." msgstr "|commands| HybridGroupコマンドでfallbackコマンドがトリガーされない問題を修正しました(:issue:`8461`, :issue:`8462`)。" -#: ../../whats_new.rst:252 +#: ../../whats_new.rst:285 msgid "Change error message for unbound callbacks in :class:`app_commands.ContextMenu` to make it clearer that bound methods are not allowed." msgstr ":class:`app_commands.ContextMenu` でバインドされていないコールバックのエラーメッセージを変更し、バインドされているメソッドは使用できないことを明確にしました。" -#: ../../whats_new.rst:253 +#: ../../whats_new.rst:286 msgid "Normalize type formatting in TypeError exceptions (:issue:`8453`)." msgstr "TypeError 例外で型のフォーマットを標準化しました。 (:issue:`8453`)" -#: ../../whats_new.rst:254 +#: ../../whats_new.rst:287 msgid "Change :meth:`VoiceProtocol.on_voice_state_update` and :meth:`VoiceProtocol.on_voice_server_update` parameters to be positional only (:issue:`8463`)." msgstr ":meth:`VoiceProtocol.on_voice_state_update` と :meth:`VoiceProtocol.on_voice_server_update` パラメータを位置指定専用に変更しました。 (:issue:`8463` )" -#: ../../whats_new.rst:255 +#: ../../whats_new.rst:288 msgid "Add support for PyCharm when using the default coloured logger (:issue:`9015`)." msgstr "デフォルトの色付きロガーに、 PyCharm 対応を追加しました。 (:issue:`9015`)" -#: ../../whats_new.rst:260 +#: ../../whats_new.rst:293 msgid "v2.0.1" msgstr "v2.0.1" -#: ../../whats_new.rst:265 +#: ../../whats_new.rst:298 msgid "Fix ``cchardet`` being installed on Python >=3.10 when using the ``speed`` extras." msgstr "Python 3.10 以降で ``speed`` extrasを使用した場合に ``cchardet`` がインストールされる問題を修正しました。" -#: ../../whats_new.rst:266 +#: ../../whats_new.rst:299 msgid "Fix :class:`ui.View` timeout updating when the :meth:`ui.View.interaction_check` failed." msgstr ":meth:`ui.View.interaction_check` に失敗したときにも :class:`ui.View` のタイムアウトが更新される問題を修正しました。" -#: ../../whats_new.rst:267 +#: ../../whats_new.rst:300 msgid "Fix :meth:`app_commands.CommandTree.on_error` not triggering if :meth:`~app_commands.CommandTree.interaction_check` raises." msgstr ":meth:`~app_commands.CommandTree.interaction_check` が例外を送出したときに :meth:`app_commands.CommandTree.on_error` が実行されない問題を修正しました。" -#: ../../whats_new.rst:268 +#: ../../whats_new.rst:301 msgid "Fix ``__main__`` script to use ``importlib.metadata`` instead of the deprecated ``pkg_resources``." msgstr "非推奨の ``pkg_resources`` の代わりに ``importlib.metadata`` を使用するよう ``__main__`` スクリプトを修正しました。" -#: ../../whats_new.rst:270 +#: ../../whats_new.rst:303 msgid "Fix library callbacks triggering a type checking error if the parameter names were different." msgstr "ライブラリコールバックのパラメータ名が異なる場合に型チェックエラーが検出される問題を修正しました。" -#: ../../whats_new.rst:270 +#: ../../whats_new.rst:303 msgid "This required a change in the :ref:`version_guarantees`" msgstr "これに伴い :ref:`version_guarantees` が改訂されました。" -#: ../../whats_new.rst:272 +#: ../../whats_new.rst:305 msgid "|commands| Fix Python 3.10 union types not working with :class:`commands.Greedy `." msgstr "|commands| Python 3.10 のユニオン型が :class:`commands.Greedy ` で動作しない問題を修正しました。" -#: ../../whats_new.rst:277 +#: ../../whats_new.rst:310 msgid "v2.0.0" msgstr "v2.0.0" -#: ../../whats_new.rst:279 +#: ../../whats_new.rst:312 msgid "The changeset for this version are too big to be listed here, for more information please see :ref:`the migrating page `." msgstr "このバージョンの変更は大きすぎるため、この場所に収まりきりません。詳細については :ref:`移行についてのページ ` を参照してください。" -#: ../../whats_new.rst:285 +#: ../../whats_new.rst:318 msgid "v1.7.3" msgstr "v1.7.3" -#: ../../whats_new.rst:290 +#: ../../whats_new.rst:323 msgid "Fix a crash involving guild uploaded stickers" msgstr "ギルドでアップロードされたスタンプに関するクラッシュを修正しました。" -#: ../../whats_new.rst:291 +#: ../../whats_new.rst:324 msgid "Fix :meth:`DMChannel.permissions_for` not having :attr:`Permissions.read_messages` set." msgstr ":meth:`DMChannel.permissions_for` に :attr:`Permissions.read_messages` が設定されていない問題を修正しました。" -#: ../../whats_new.rst:296 +#: ../../whats_new.rst:329 msgid "v1.7.2" msgstr "v1.7.2" -#: ../../whats_new.rst:301 +#: ../../whats_new.rst:334 msgid "Fix ``fail_if_not_exists`` causing certain message references to not be usable within :meth:`abc.Messageable.send` and :meth:`Message.reply` (:issue:`6726`)" msgstr "``fail_if_not_exists`` により、特定のメッセージ参照が :meth:`abc.Messageable.send` および :meth:`Message.reply` 内で使用できない問題を修正しました。 (:issue:`6726`)" -#: ../../whats_new.rst:302 +#: ../../whats_new.rst:335 msgid "Fix :meth:`Guild.chunk` hanging when the user left the guild. (:issue:`6730`)" msgstr "ギルドからユーザーが脱退した際に :meth:`Guild.chunk` がハングするのを修正しました。 (:issue:`6730`)" -#: ../../whats_new.rst:303 +#: ../../whats_new.rst:336 msgid "Fix loop sleeping after final iteration rather than before (:issue:`6744`)" msgstr "最終反復の前ではなく後にループがスリープするのを修正しました。 (:issue:`6744`)" -#: ../../whats_new.rst:308 +#: ../../whats_new.rst:341 msgid "v1.7.1" msgstr "v1.7.1" -#: ../../whats_new.rst:313 +#: ../../whats_new.rst:346 msgid "|commands| Fix :meth:`Cog.has_error_handler ` not working as intended." msgstr "|commands| :meth:`Cog.has_error_handler ` が正常に動作しない問題を修正しました。" -#: ../../whats_new.rst:318 +#: ../../whats_new.rst:351 msgid "v1.7.0" msgstr "v1.7.0" -#: ../../whats_new.rst:320 +#: ../../whats_new.rst:353 msgid "This version is mainly for improvements and bug fixes. This is more than likely the last major version in the 1.x series. Work after this will be spent on v2.0. As a result, **this is the last version to support Python 3.5**. Likewise, **this is the last version to support user bots**." msgstr "このバージョンは、主にバグ修正と、機能改善が含まれています。 おそらくこのバージョンが、1.xシリーズの最後のメジャーバージョンとなる予定です。これ以降の作業は、主にv2.0に費やされます。 結果として、**このバージョンが、Python 3.5をサポートする最後のバージョンになります**。 同様に、**このバージョンがユーザーボットをサポートする最後のバージョンです**。" -#: ../../whats_new.rst:324 +#: ../../whats_new.rst:357 msgid "Development of v2.0 will have breaking changes and support for newer API features." msgstr "v2.0の開発には、破壊的更新と、新しいAPI機能の変更が含まれるでしょう。" -#: ../../whats_new.rst:329 +#: ../../whats_new.rst:362 msgid "Add support for stage channels via :class:`StageChannel` (:issue:`6602`, :issue:`6608`)" msgstr ":class:`StageChannel` のサポートを追加しました。 (:issue:`6602`, :issue:`6608`)" -#: ../../whats_new.rst:332 +#: ../../whats_new.rst:365 msgid "Add support for :attr:`MessageReference.fail_if_not_exists` (:issue:`6484`)" msgstr ":attr:`MessageReference.fail_if_not_exists` のサポートを追加しました。 (:issue:`6484`)" -#: ../../whats_new.rst:331 +#: ../../whats_new.rst:364 msgid "By default, if the message you're replying to doesn't exist then the API errors out. This attribute tells the Discord API that it's okay for that message to be missing." msgstr "デフォルトでは、もし返信するメッセージが存在しない場合、APIエラーが発生します。この属性は、Discord APIにメッセージが存在していない場合でも、問題がないことを伝えます。" -#: ../../whats_new.rst:334 +#: ../../whats_new.rst:367 msgid "Add support for Discord's new permission serialisation scheme." msgstr "Discordの新しい権限シリアライゼーションスキームのサポートを追加しました。" -#: ../../whats_new.rst:335 +#: ../../whats_new.rst:368 msgid "Add an easier way to move channels using :meth:`abc.GuildChannel.move`" msgstr "簡単にチャンネルの移動をする :meth:`abc.GuildChannel.move` を追加しました。" -#: ../../whats_new.rst:336 +#: ../../whats_new.rst:369 msgid "Add :attr:`Permissions.use_slash_commands`" msgstr "新しい権限 :attr:`Permissions.use_slash_commands` を追加しました。" -#: ../../whats_new.rst:337 +#: ../../whats_new.rst:370 msgid "Add :attr:`Permissions.request_to_speak`" msgstr "新しい権限 :attr:`Permissions.request_to_speak` を追加しました。" -#: ../../whats_new.rst:338 +#: ../../whats_new.rst:371 msgid "Add support for voice regions in voice channels via :attr:`VoiceChannel.rtc_region` (:issue:`6606`)" msgstr ":attr:`VoiceChannel.rtc_region` によるボイスチャンネルの、ボイスリージョンのサポートを追加しました。 (:issue:`6606`)" -#: ../../whats_new.rst:339 +#: ../../whats_new.rst:372 msgid "Add support for :meth:`PartialEmoji.url_as` (:issue:`6341`)" msgstr ":meth:`PartialEmoji.url_as` のサポートを追加しました。 (:issue:`6341`)" -#: ../../whats_new.rst:340 +#: ../../whats_new.rst:373 msgid "Add :attr:`MessageReference.jump_url` (:issue:`6318`)" msgstr ":attr:`MessageReference.jump_url` を追加しました。(:issue:`6318`)" -#: ../../whats_new.rst:341 +#: ../../whats_new.rst:374 msgid "Add :attr:`File.spoiler` (:issue:`6317`)" msgstr ":attr:`File.spoiler` を追加しました。 (:issue:`6317`)" -#: ../../whats_new.rst:342 +#: ../../whats_new.rst:375 msgid "Add support for passing ``roles`` to :meth:`Guild.estimate_pruned_members` (:issue:`6538`)" msgstr "``roles`` を :meth:`Guild.estimate_pruned_members` に渡すことができるようになりました。(:issue:`6538`)" -#: ../../whats_new.rst:343 +#: ../../whats_new.rst:376 msgid "Allow callable class factories to be used in :meth:`abc.Connectable.connect` (:issue:`6478`)" msgstr ":meth:`abc.Connectable.connect` において、クラスを生成する呼び出し可能オブジェクトを使用できるようになりました。( :issue:`6478` )" -#: ../../whats_new.rst:344 +#: ../../whats_new.rst:377 msgid "Add a way to get mutual guilds from the client's cache via :attr:`User.mutual_guilds` (:issue:`2539`, :issue:`6444`)" msgstr "クライアントのキャッシュから共通のギルドを取得する :attr:`User.mutual_guilds` を追加しました。 (:issue:`2539`, :issue:`6444`)" -#: ../../whats_new.rst:345 +#: ../../whats_new.rst:378 msgid ":meth:`PartialMessage.edit` now returns a full :class:`Message` upon success (:issue:`6309`)" msgstr ":meth:`PartialMessage.edit` が成功時に完全な :class:`Message` を返すようになりました。 (:issue:`6309`)" -#: ../../whats_new.rst:346 +#: ../../whats_new.rst:379 msgid "Add :attr:`RawMessageUpdateEvent.guild_id` (:issue:`6489`)" msgstr ":attr:`RawMessageUpdateEvent.guild_id` を追加しました。(:issue:`6489`)" -#: ../../whats_new.rst:347 +#: ../../whats_new.rst:380 msgid ":class:`AuditLogEntry` is now hashable (:issue:`6495`)" msgstr ":class:`AuditLogEntry` がハッシュ可能になりました。 (:issue:`6495`)" -#: ../../whats_new.rst:348 +#: ../../whats_new.rst:381 msgid ":class:`Attachment` is now hashable" msgstr ":class:`Attachment` がハッシュ可能になりました。" -#: ../../whats_new.rst:349 +#: ../../whats_new.rst:382 msgid "Add :attr:`Attachment.content_type` attribute (:issue:`6618`)" msgstr ":attr:`Attachment.content_type` 属性を追加しました。 (:issue:`6618`)" -#: ../../whats_new.rst:350 +#: ../../whats_new.rst:383 msgid "Add support for casting :class:`Attachment` to :class:`str` to get the URL." msgstr "URLを取得するために :class:`Atachment` を :class:`str` へキャストできるようになりました。" -#: ../../whats_new.rst:352 +#: ../../whats_new.rst:385 msgid "Add ``seed`` parameter for :class:`Colour.random` (:issue:`6562`)" msgstr ":class:`Colour.random` に ``seed`` パラメータを追加しました。 (:issue:`6562`)" -#: ../../whats_new.rst:352 +#: ../../whats_new.rst:385 msgid "This only seeds it for one call. If seeding for multiple calls is desirable, use :func:`random.seed`." msgstr "これは1つの呼び出しに対してのみシードします。複数の呼び出しに対するシードが望ましい場合は、 :func:`random.seed` を使用してください。" -#: ../../whats_new.rst:354 +#: ../../whats_new.rst:387 msgid "Add a :func:`utils.remove_markdown` helper function (:issue:`6573`)" msgstr ":func:`utils.remove_markdown` ヘルパー関数を追加しました。 (:issue:`6573`)" -#: ../../whats_new.rst:355 +#: ../../whats_new.rst:388 msgid "Add support for passing scopes to :func:`utils.oauth_url` (:issue:`6568`)" msgstr ":func:`utils.oauth_url` にスコープを渡すことが可能になりました。 (:issue:`6568`)" -#: ../../whats_new.rst:356 +#: ../../whats_new.rst:389 msgid "|commands| Add support for ``rgb`` CSS function as a parameter to :class:`ColourConverter ` (:issue:`6374`)" msgstr "|commands| :class:`ColourConverter ` において、 ``rgb`` CSS関数の文字列を変換できるようになりました。 (:issue:`6374`)" -#: ../../whats_new.rst:357 +#: ../../whats_new.rst:390 msgid "|commands| Add support for converting :class:`StoreChannel` via :class:`StoreChannelConverter ` (:issue:`6603`)" msgstr "|commands| :class:`StoreChannel` を :class:`StoreChannelConverter ` によって変換できるようになりました。 (:issue:`6603`)" -#: ../../whats_new.rst:358 +#: ../../whats_new.rst:391 msgid "|commands| Add support for stripping whitespace after the prefix is encountered using the ``strip_after_prefix`` :class:`~ext.commands.Bot` constructor parameter." msgstr "|commands| :class:`~ext.commands.Bot` の ``strip_after_prefix`` 初期化パラメーターを指定することで、プレフィックスのあとの空白を取り除けるようになりました。" -#: ../../whats_new.rst:359 +#: ../../whats_new.rst:392 msgid "|commands| Add :attr:`Context.invoked_parents ` to get the aliases a command's parent was invoked with (:issue:`1874`, :issue:`6462`)" msgstr "|commands| :attr:`Context.invoked_parents ` で、呼び出されたときのコマンドの親のエイリアスを取得できるようになりました。 (:issue:`1874`, :issue:`6462`)" -#: ../../whats_new.rst:360 +#: ../../whats_new.rst:393 msgid "|commands| Add a converter for :class:`PartialMessage` under :class:`ext.commands.PartialMessageConverter` (:issue:`6308`)" msgstr "|commands| :class:`PartialMessage` 用のコンバーター :class:`ext.commands.PartialMessageConverter` を追加しました。 (:issue:`6308`)" -#: ../../whats_new.rst:361 +#: ../../whats_new.rst:394 msgid "|commands| Add a converter for :class:`Guild` under :class:`ext.commands.GuildConverter` (:issue:`6016`, :issue:`6365`)" msgstr "|commands| :class:`Guild` 用のコンバーター :class:`ext.commands.GuildConverter` を追加しました。 (:issue:`6016`, :issue:`6365`)" -#: ../../whats_new.rst:362 +#: ../../whats_new.rst:395 msgid "|commands| Add :meth:`Command.has_error_handler `" msgstr "|commands| :meth:`Command.has_error_handler ` を追加しました。" -#: ../../whats_new.rst:363 +#: ../../whats_new.rst:396 msgid "This is also adds :meth:`Cog.has_error_handler `" msgstr ":meth:`Cog.has_error_handler ` も追加されました。" -#: ../../whats_new.rst:364 +#: ../../whats_new.rst:397 msgid "|commands| Allow callable types to act as a bucket key for cooldowns (:issue:`6563`)" msgstr "|commands| 呼び出し可能オブジェクトをクールダウンのバケットキーとして使用できるようになりました。 (:issue:`6563`)" -#: ../../whats_new.rst:365 +#: ../../whats_new.rst:398 msgid "|commands| Add ``linesep`` keyword argument to :class:`Paginator ` (:issue:`5975`)" msgstr "|commands| :class:`Paginator ` に ``linesep`` キーワード引数を追加しました。 (:issue:`5975`)" -#: ../../whats_new.rst:366 +#: ../../whats_new.rst:399 msgid "|commands| Allow ``None`` to be passed to :attr:`HelpCommand.verify_checks ` to only verify in a guild context (:issue:`2008`, :issue:`6446`)" msgstr "|commands| :attr:`HelpCommand.verify_checks ` に ``None`` を指定することで、サーバー内でのみチェックを確認するようにできるようにしました。 (:issue:`2008`, :issue:`6446`)" -#: ../../whats_new.rst:367 +#: ../../whats_new.rst:400 msgid "|commands| Allow relative paths when loading extensions via a ``package`` keyword argument (:issue:`2465`, :issue:`6445`)" msgstr "|commands| ``package`` キーワード引数で、エクステンションをロードするときの相対パスを指定できるようになりました。 (:issue:`2465`, :issue:`6445`)" -#: ../../whats_new.rst:372 +#: ../../whats_new.rst:405 msgid "Fix mentions not working if ``mention_author`` is passed in :meth:`abc.Messageable.send` without :attr:`Client.allowed_mentions` set (:issue:`6192`, :issue:`6458`)" msgstr ":attr:`Client.allowed_mentions` が設定されていないときに、 :meth:`abc.Messageable.send` で ``mention_author`` が渡されてもメンションしない問題を修正しました。 (:issue:`6192`, :issue:`6458`)" -#: ../../whats_new.rst:373 +#: ../../whats_new.rst:406 msgid "Fix user created instances of :class:`CustomActivity` triggering an error (:issue:`4049`)" msgstr "ユーザーが作成した :class:`CustomActivity` インスタンスがエラーを引き起こす問題を修正しました。 (:issue:`4049`)" -#: ../../whats_new.rst:374 +#: ../../whats_new.rst:407 msgid "Note that currently, bot users still cannot set a custom activity due to a Discord limitation." msgstr "現在、Discordの制限により、Botはまだカスタムアクティビティを設定できません。" -#: ../../whats_new.rst:375 +#: ../../whats_new.rst:408 msgid "Fix :exc:`ZeroDivisionError` being raised from :attr:`VoiceClient.average_latency` (:issue:`6430`, :issue:`6436`)" msgstr ":attr:`VoiceClient.average_latency` にて :exc:`ZeroDivisionError` が発生する問題を修正しました。 (:issue:`6430`, :issue:`6436`)" -#: ../../whats_new.rst:376 +#: ../../whats_new.rst:409 msgid "Fix :attr:`User.public_flags` not updating upon edit (:issue:`6315`)" msgstr ":attr:`User.public_flags` が編集時に更新されない問題を修正しました。 (:issue:`6315`)" -#: ../../whats_new.rst:377 +#: ../../whats_new.rst:410 msgid "Fix :attr:`Message.call` sometimes causing attribute errors (:issue:`6390`)" msgstr ":attr:`Message.call` が時々AttributeErrorを送出する問題を修正しました。 (:issue:`6390`)" -#: ../../whats_new.rst:378 +#: ../../whats_new.rst:411 msgid "Fix issue resending a file during request retries on newer versions of ``aiohttp`` (:issue:`6531`)" msgstr "新しいバージョンの ``aiohttp`` で、リクエストの再試行中にファイルを再送するときに発生する問題を修正しました。 (:issue:`6531`)" -#: ../../whats_new.rst:379 +#: ../../whats_new.rst:412 msgid "Raise an error when ``user_ids`` is empty in :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` を呼び出す際、 ``user_ids`` に空のリストが指定された際にエラーが発生するようになりました。" -#: ../../whats_new.rst:380 +#: ../../whats_new.rst:413 msgid "Fix ``__str__`` magic method raising when a :class:`Guild` is unavailable." msgstr ":class:`Guild` が利用不可能なときに ``__str__`` メソッドがエラーを出す問題を修正しました。" -#: ../../whats_new.rst:381 +#: ../../whats_new.rst:414 msgid "Fix potential :exc:`AttributeError` when accessing :attr:`VoiceChannel.members` (:issue:`6602`)" msgstr ":attr:`VoiceChannel.members` にアクセスする時に :exc:`AttributeError` が発生する潜在的なバグを修正しました。(:issue:`6602`)" -#: ../../whats_new.rst:382 +#: ../../whats_new.rst:415 msgid ":class:`Embed` constructor parameters now implicitly convert to :class:`str` (:issue:`6574`)" msgstr ":class:`Embed` の初期化時に指定された引数は暗黙的に :class:`str` へ変換されるようになりました。 (:issue:`6574`)" -#: ../../whats_new.rst:383 +#: ../../whats_new.rst:416 msgid "Ensure ``discord`` package is only run if executed as a script (:issue:`6483`)" msgstr "``discord`` パッケージがスクリプトとして実行された場合のみ実行されるようになりました。 (:issue:`6483`)" -#: ../../whats_new.rst:384 +#: ../../whats_new.rst:417 msgid "|commands| Fix irrelevant commands potentially being unloaded during cog unload due to failure." msgstr "|commands| コグのアンロード中、失敗することにより無関係なコマンドがアンロードされる可能性がある問題を修正しました。" -#: ../../whats_new.rst:385 +#: ../../whats_new.rst:418 msgid "|commands| Fix attribute errors when setting a cog to :class:`~.ext.commands.HelpCommand` (:issue:`5154`)" msgstr "|commands| コグを :class:`~.ext.commands.HelpCommand` に設定した際にAttributeErrorが出る問題を修正しました。 (:issue:`5154`)" -#: ../../whats_new.rst:386 +#: ../../whats_new.rst:419 msgid "|commands| Fix :attr:`Context.invoked_with ` being improperly reassigned during a :meth:`~ext.commands.Context.reinvoke` (:issue:`6451`, :issue:`6462`)" msgstr "|commands| :meth:`~ext.commands.Context.reinvoke` 中に :attr:`Context.invoked_with ` が不適切に再割り当てされる問題を修正しました。 (:issue:`6451`, :issue:`6462`)" -#: ../../whats_new.rst:387 +#: ../../whats_new.rst:420 msgid "|commands| Remove duplicates from :meth:`HelpCommand.get_bot_mapping ` (:issue:`6316`)" msgstr "|commands| :meth:`HelpCommand.get_bot_mapping ` で、コマンドが重複する問題を修正しました。 (:issue:`6316`)" -#: ../../whats_new.rst:388 +#: ../../whats_new.rst:421 msgid "|commands| Properly handle positional-only parameters in bot command signatures (:issue:`6431`)" msgstr "|commands| Botのコマンドシグネチャーで、位置限定引数を適切に処理するようになりました。 (:issue:`6431`)" -#: ../../whats_new.rst:389 +#: ../../whats_new.rst:422 msgid "|commands| Group signatures now properly show up in :attr:`Command.signature ` (:issue:`6529`, :issue:`6530`)" msgstr "|commands| グループのシグネチャーが :attr:`Command.signature ` に正しく表示されるようになりました。 (:issue:`6529`, :issue:`6530`)" -#: ../../whats_new.rst:394 +#: ../../whats_new.rst:427 msgid "User endpoints and all userbot related functionality has been deprecated and will be removed in the next major version of the library." msgstr "ユーザー用エンドポイントとユーザーボットの関連機能は非推奨になり、次のライブラリのメジャーバージョンで削除されます。" -#: ../../whats_new.rst:395 +#: ../../whats_new.rst:428 msgid ":class:`Permission` class methods were updated to match the UI of the Discord client (:issue:`6476`)" msgstr ":class:`Permission` のクラスメソッドがDiscordクライアントのUIと一致するように更新されました。 (:issue:`6476`)" -#: ../../whats_new.rst:396 +#: ../../whats_new.rst:429 msgid "``_`` and ``-`` characters are now stripped when making a new cog using the ``discord`` package (:issue:`6313`)" msgstr "``_`` と ``-`` の文字が ``discord`` パッケージを使用して新しいコグを作成するときに取り除かれるようになりました。 (:issue:`6313`)" -#: ../../whats_new.rst:401 +#: ../../whats_new.rst:434 msgid "v1.6.0" msgstr "v1.6.0" -#: ../../whats_new.rst:403 +#: ../../whats_new.rst:436 msgid "This version comes with support for replies and stickers." msgstr "このバージョンでは、返信機能とスタンプ機能がサポートされるようになりました。" -#: ../../whats_new.rst:408 +#: ../../whats_new.rst:441 msgid "An entirely redesigned documentation. This was the cumulation of multiple months of effort." msgstr "完全に再設計されたドキュメント。 これは何ヶ月もの努力の積み重ねで作られました。" -#: ../../whats_new.rst:409 +#: ../../whats_new.rst:442 msgid "There's now a dark theme, feel free to navigate to the cog on the screen to change your setting, though this should be automatic." msgstr "ダークテーマが実装されました。変更するには、画面上の歯車から設定をしてください。これは自動的に行われます。" -#: ../../whats_new.rst:410 +#: ../../whats_new.rst:443 msgid "Add support for :meth:`AppInfo.icon_url_as` and :meth:`AppInfo.cover_image_url_as` (:issue:`5888`)" msgstr ":meth:`AppInfo.icon_url_as` と :meth:`AppInfo.cover_image_url_as` が追加されました。 (:issue:`5888`)" -#: ../../whats_new.rst:411 +#: ../../whats_new.rst:444 msgid "Add :meth:`Colour.random` to get a random colour (:issue:`6067`)" msgstr "ランダムな色が得られる、 :meth:`Colour.random` が追加されました。 (:issue:`6067`)" -#: ../../whats_new.rst:412 +#: ../../whats_new.rst:445 msgid "Add support for stickers via :class:`Sticker` (:issue:`5946`)" msgstr ":class:`Sticker` によってスタンプがサポートされました。 (:issue:`5946`)" -#: ../../whats_new.rst:416 +#: ../../whats_new.rst:449 msgid "Add support for replying via :meth:`Message.reply` (:issue:`6061`)" msgstr ":meth:`Message.reply` で返信ができるようになりました。 (:issue:`6061`)" -#: ../../whats_new.rst:414 +#: ../../whats_new.rst:447 msgid "This also comes with the :attr:`AllowedMentions.replied_user` setting." msgstr "これには :attr:`AllowedMentions.replied_user` の設定も含まれます。" -#: ../../whats_new.rst:415 +#: ../../whats_new.rst:448 msgid ":meth:`abc.Messageable.send` can now accept a :class:`MessageReference`." msgstr ":meth:`abc.Messageable.send` が :class:`MessageReference` を受け付けるようになりました。" -#: ../../whats_new.rst:416 +#: ../../whats_new.rst:449 msgid ":class:`MessageReference` can now be constructed by users." msgstr ":class:`MessageReference` がユーザーによって生成できるようになりました。" -#: ../../whats_new.rst:417 +#: ../../whats_new.rst:450 msgid ":meth:`Message.to_reference` can now convert a message to a :class:`MessageReference`." msgstr ":meth:`Message.to_reference` によってMessageオブジェクトを :class:`MessageReference` に変換できるようになりました。" -#: ../../whats_new.rst:418 +#: ../../whats_new.rst:451 msgid "Add support for getting the replied to resolved message through :attr:`MessageReference.resolved`." msgstr ":attr:`MessageReference.resolved` で解決済みメッセージを得ることができます。" -#: ../../whats_new.rst:424 +#: ../../whats_new.rst:457 msgid "Add support for role tags." msgstr "ロールのタグがサポートされました。" -#: ../../whats_new.rst:420 +#: ../../whats_new.rst:453 msgid ":attr:`Guild.premium_subscriber_role` to get the \"Nitro Booster\" role (if available)." msgstr ":attr:`Guild.premium_subscriber_role` で ニトロブースターロールを取得できます(利用可能な場合)。" -#: ../../whats_new.rst:421 +#: ../../whats_new.rst:454 msgid ":attr:`Guild.self_role` to get the bot's own role (if available)." msgstr ":attr:`Guild.self_role` でサーバー内のBot自身のロールを取得できます(利用可能な場合)。" -#: ../../whats_new.rst:422 +#: ../../whats_new.rst:455 msgid ":attr:`Role.tags` to get the role's tags." msgstr ":attr:`Role.tags` でロールのタグを取得できます。" -#: ../../whats_new.rst:423 +#: ../../whats_new.rst:456 msgid ":meth:`Role.is_premium_subscriber` to check if a role is the \"Nitro Booster\" role." msgstr ":meth:`Role.is_premium_subscriber` でロールがニトロブースターロールであるかを確認できます。" -#: ../../whats_new.rst:424 +#: ../../whats_new.rst:457 msgid ":meth:`Role.is_bot_managed` to check if a role is a bot role (i.e. the automatically created role for bots)." msgstr ":meth:`Role.is_bot_managed` でロールがボットロール(自動的に作られたBot用ロール)であるかを確認できます。" -#: ../../whats_new.rst:425 +#: ../../whats_new.rst:458 msgid ":meth:`Role.is_integration` to check if a role is role created by an integration." msgstr ":meth:`Role.is_integration` でインテグレーションによって作成されたロールかどうか確認できます。" -#: ../../whats_new.rst:426 +#: ../../whats_new.rst:459 msgid "Add :meth:`Client.is_ws_ratelimited` to check if the websocket is rate limited." msgstr ":meth:`Client.is_ws_ratelimited` でWebSocketのレート制限がされているかどうか確認できるようになりました。" -#: ../../whats_new.rst:427 +#: ../../whats_new.rst:460 msgid ":meth:`ShardInfo.is_ws_ratelimited` is the equivalent for checking a specific shard." msgstr ":meth:`ShardInfo.is_ws_ratelimited` は特定のシャードのWebSocketレート制限をチェックします。" -#: ../../whats_new.rst:428 +#: ../../whats_new.rst:461 msgid "Add support for chunking an :class:`AsyncIterator` through :meth:`AsyncIterator.chunk` (:issue:`6100`, :issue:`6082`)" msgstr ":class:`AsyncIterator` を :meth:`AsyncIterator.chunk` を通してチャンク化できるようになりました。 (:issue:`6100`, :issue:`6082`)" -#: ../../whats_new.rst:429 +#: ../../whats_new.rst:462 msgid "Add :attr:`PartialEmoji.created_at` (:issue:`6128`)" msgstr ":attr:`PartialEmoji.created_at` を追加しました。 (:issue:`6128`)" -#: ../../whats_new.rst:430 +#: ../../whats_new.rst:463 msgid "Add support for editing and deleting webhook sent messages (:issue:`6058`)" msgstr "Webhookで送信したメッセージの編集と削除をサポートしました。 (:issue:`6058`)" -#: ../../whats_new.rst:431 +#: ../../whats_new.rst:464 msgid "This adds :class:`WebhookMessage` as well to power this behaviour." msgstr "この機能のために :class:`WebhookMessage` が追加されました。" -#: ../../whats_new.rst:432 +#: ../../whats_new.rst:465 msgid "Add :class:`PartialMessage` to allow working with a message via channel objects and just a message_id (:issue:`5905`)" msgstr "チャンネルオブジェクトとメッセージIDのみでメッセージを操作できるようにするために、 :class:`PartialMessage` を追加しました。 (:issue:`5905`)" -#: ../../whats_new.rst:433 +#: ../../whats_new.rst:466 msgid "This is useful if you don't want to incur an extra API call to fetch the message." msgstr "これはメッセージを取得するために追加のAPI呼び出しをしたくないときに便利です。" -#: ../../whats_new.rst:434 +#: ../../whats_new.rst:467 msgid "Add :meth:`Emoji.url_as` (:issue:`6162`)" msgstr ":meth:`Emoji.url_as` を追加しました。 (:issue:`6162`)" -#: ../../whats_new.rst:435 +#: ../../whats_new.rst:468 msgid "Add support for :attr:`Member.pending` for the membership gating feature." msgstr "メンバーシップゲート機能用に :attr:`Member.pending` のサポートを追加しました。" -#: ../../whats_new.rst:436 +#: ../../whats_new.rst:469 msgid "Allow ``colour`` parameter to take ``int`` in :meth:`Guild.create_role` (:issue:`6195`)" msgstr ":meth:`Guild.create_role` で ``colour`` パラメータに ``int`` 型を渡すことができるようになりました。 (:issue:`6195`)" -#: ../../whats_new.rst:437 +#: ../../whats_new.rst:470 msgid "Add support for ``presences`` in :meth:`Guild.query_members` (:issue:`2354`)" msgstr ":meth:`Guild.query_members` で、 ``presences`` 引数が使えるようになりました。 (:issue:`2354`)" -#: ../../whats_new.rst:438 +#: ../../whats_new.rst:471 msgid "|commands| Add support for ``description`` keyword argument in :class:`commands.Cog ` (:issue:`6028`)" msgstr "|commands| :class:`commands.Cog ` において、 ``description`` キーワード引数が使えるようになりました。 (:issue:`6028`)" -#: ../../whats_new.rst:439 +#: ../../whats_new.rst:472 msgid "|tasks| Add support for calling the wrapped coroutine as a function via ``__call__``." msgstr "|tasks| ``__call__`` を使うことによってラップされたコルーチン関数を呼び出せるようになりました。" -#: ../../whats_new.rst:445 +#: ../../whats_new.rst:478 msgid "Raise :exc:`DiscordServerError` when reaching 503s repeatedly (:issue:`6044`)" msgstr "HTTPリクエスト時にステータス503が繰り返し返されたとき、 :exc:`DiscordServerError` が出るようになりました。 (:issue:`6044`)" -#: ../../whats_new.rst:446 +#: ../../whats_new.rst:479 msgid "Fix :exc:`AttributeError` when :meth:`Client.fetch_template` is called (:issue:`5986`)" msgstr ":meth:`Client.fetch_template` が呼び出されたとき :exc:`AttributeError` が出る問題を修正しました。 (:issue:`5986`)" -#: ../../whats_new.rst:447 +#: ../../whats_new.rst:480 msgid "Fix errors when playing audio and moving to another channel (:issue:`5953`)" msgstr "音声を再生するときと別のボイスチャンネルへ移動するときに発生するエラーを修正しました。 (:issue:`5953`)" -#: ../../whats_new.rst:448 +#: ../../whats_new.rst:481 msgid "Fix :exc:`AttributeError` when voice channels disconnect too fast (:issue:`6039`)" msgstr "ボイスチャンネルから切断するのが速すぎるときに発生する :exc:`AttributeError` を修正しました。 (:issue:`6039`)" -#: ../../whats_new.rst:449 +#: ../../whats_new.rst:482 msgid "Fix stale :class:`User` references when the members intent is off." msgstr "memberインテントがオフの場合に :class:`User` の参照が古くなってしまう問題を修正しました。" -#: ../../whats_new.rst:450 +#: ../../whats_new.rst:483 msgid "Fix :func:`on_user_update` not dispatching in certain cases when a member is not cached but the user somehow is." msgstr "memberがキャッシュされておらず、userが何らかの形でキャッシュされている場合に :func:`on_user_update` が発火されない問題を修正しました。" -#: ../../whats_new.rst:451 +#: ../../whats_new.rst:484 msgid "Fix :attr:`Message.author` being overwritten in certain cases during message update." msgstr "メッセージが更新されているとき、特定のケースで :attr:`Message.author` が上書きされてしまう問題を修正しました。" -#: ../../whats_new.rst:452 +#: ../../whats_new.rst:485 msgid "This would previously make it so :attr:`Message.author` is a :class:`User`." msgstr "これにより、 :attr:`Message.author` が :class:`User` になるようになりました。" -#: ../../whats_new.rst:453 +#: ../../whats_new.rst:486 msgid "Fix :exc:`UnboundLocalError` for editing ``public_updates_channel`` in :meth:`Guild.edit` (:issue:`6093`)" msgstr ":meth:`Guild.edit` で ``public_updates_channel`` を変更する際に :exc:`UnboundLocalError` が発生する問題を修正しました。 (:issue:`6093`)" -#: ../../whats_new.rst:454 +#: ../../whats_new.rst:487 msgid "Fix uninitialised :attr:`CustomActivity.created_at` (:issue:`6095`)" msgstr ":attr:`CustomActivity.created_at` が初期化されない問題を修正しました。 (:issue:`6095`)" -#: ../../whats_new.rst:455 +#: ../../whats_new.rst:488 msgid "|commands| Errors during cog unload no longer stops module cleanup (:issue:`6113`)" msgstr "|commands| コグのアンロード中に起きたエラーがモジュールのcleanupを止めないようになりました。 (:issue:`6113`)" -#: ../../whats_new.rst:456 +#: ../../whats_new.rst:489 msgid "|commands| Properly cleanup lingering commands when a conflicting alias is found when adding commands (:issue:`6217`)" msgstr "|commands| コマンドを追加する際、エイリアスが競合したときに残ってしまうエイリアスを適切にクリーンアップするようになりました。 (:issue:`6217`)" -#: ../../whats_new.rst:461 +#: ../../whats_new.rst:494 msgid "``ffmpeg`` spawned processes no longer open a window in Windows (:issue:`6038`)" msgstr "Windowsにおいて呼び出された ``ffmpeg`` がウィンドウを開かないようになりました。 (:issue:`6038`)" -#: ../../whats_new.rst:462 +#: ../../whats_new.rst:495 msgid "Update dependencies to allow the library to work on Python 3.9+ without requiring build tools. (:issue:`5984`, :issue:`5970`)" msgstr "ライブラリがビルドツールなしでPython3.9以上で動作するよう、依存関係を変更しました。 (:issue:`5984`, :issue:`5970`)" -#: ../../whats_new.rst:463 +#: ../../whats_new.rst:496 msgid "Fix docstring issue leading to a SyntaxError in 3.9 (:issue:`6153`)" msgstr "Python3.9においてSyntaxErrorになるdocstringの問題を修正しました。 (:issue:`6153`)" -#: ../../whats_new.rst:464 +#: ../../whats_new.rst:497 msgid "Update Windows opus binaries from 1.2.1 to 1.3.1 (:issue:`6161`)" msgstr "Windows用のopusバイナリをバージョン1.2.1から1.3.1に更新しました。 (:issue:`6161`)" -#: ../../whats_new.rst:465 +#: ../../whats_new.rst:498 msgid "Allow :meth:`Guild.create_role` to accept :class:`int` as the ``colour`` parameter (:issue:`6195`)" msgstr ":meth:`Guild.create_role` の ``colour`` 引数で :class:`int` 型が使えるようになりました。\n" "(:issue:`6195`)" -#: ../../whats_new.rst:466 +#: ../../whats_new.rst:499 msgid "|commands| :class:`MessageConverter ` regex got updated to support ``www.`` prefixes (:issue:`6002`)" msgstr "|commands| :class:`MessageConverter ` のregexが ``www.`` プレフィックスをサポートするように更新されました。 (:issue:`6002`)" -#: ../../whats_new.rst:467 +#: ../../whats_new.rst:500 msgid "|commands| :class:`UserConverter ` now fetches the API if an ID is passed and the user is not cached." msgstr "|commands| :class:`UserConverter ` は、IDが渡され、そのユーザーがキャッシュされていない場合にAPIからデータを取得するようになりました。" -#: ../../whats_new.rst:468 +#: ../../whats_new.rst:501 msgid "|commands| :func:`max_concurrency ` is now called before cooldowns (:issue:`6172`)" msgstr "|commands| :func:`max_concurrency ` がクールダウンの前に呼び出されるようになりました。 (:issue:`6172`)" -#: ../../whats_new.rst:473 +#: ../../whats_new.rst:506 msgid "v1.5.1" msgstr "v1.5.1" -#: ../../whats_new.rst:478 +#: ../../whats_new.rst:511 msgid "Fix :func:`utils.escape_markdown` not escaping quotes properly (:issue:`5897`)" msgstr ":func:`utils.escape_markdown` が引用符を正しくエスケープしない問題を修正しました。 (:issue:`5897`)" -#: ../../whats_new.rst:479 +#: ../../whats_new.rst:512 msgid "Fix :class:`Message` not being hashable (:issue:`5901`, :issue:`5866`)" msgstr ":class:`Message` がハッシュ可能でない問題を修正しました。 (:issue:`5901`, :issue:`5866`)" -#: ../../whats_new.rst:480 +#: ../../whats_new.rst:513 msgid "Fix moving channels to the end of the channel list (:issue:`5923`)" msgstr "チャンネルをチャンネルリストの最後まで移動する際の問題を修正しました。 (:issue:`5923`)" -#: ../../whats_new.rst:481 +#: ../../whats_new.rst:514 msgid "Fix seemingly strange behaviour in ``__eq__`` for :class:`PermissionOverwrite` (:issue:`5929`)" msgstr ":class:`PermissionOverwrite` における ``__eq__`` のおかしい挙動を修正しました。 (:issue:`5929`)" -#: ../../whats_new.rst:482 +#: ../../whats_new.rst:515 msgid "Fix aliases showing up in ``__iter__`` for :class:`Intents` (:issue:`5945`)" msgstr ":class:`Intents` の ``__iter__`` におけるエイリアスの表示の問題を修正しました。 (:issue:`5945`)" -#: ../../whats_new.rst:483 +#: ../../whats_new.rst:516 msgid "Fix the bot disconnecting from voice when moving them to another channel (:issue:`5904`)" msgstr "別のボイスチャンネルに移動する時にBotがボイスチャンネルから切断されてしまう問題を修正しました。 (:issue:`5945`)" -#: ../../whats_new.rst:484 +#: ../../whats_new.rst:517 msgid "Fix attribute errors when chunking times out sometimes during delayed on_ready dispatching." msgstr "遅延on_readyディスパッチ中にチャンキングがタイムアウトする場合の属性エラーを修正しました。" -#: ../../whats_new.rst:485 +#: ../../whats_new.rst:518 msgid "Ensure that the bot's own member is not evicted from the cache (:issue:`5949`)" msgstr "Bot自身のmemberオブジェクトがキャッシュから削除されないことが保証されるようになりました。 (:issue:`5949`)" -#: ../../whats_new.rst:490 +#: ../../whats_new.rst:523 msgid "Members are now loaded during ``GUILD_MEMBER_UPDATE`` events if :attr:`MemberCacheFlags.joined` is set. (:issue:`5930`)" msgstr ":attr:`MemberCacheFlags.joined` が設定されている場合、memberが ``GUILD_MEMBER_UPDATE`` イベントでロードされるようになりました。 (:issue:`5930`)" -#: ../../whats_new.rst:491 +#: ../../whats_new.rst:524 msgid "|commands| :class:`MemberConverter ` now properly lazily fetches members if not available from cache." msgstr "|commands| :class:`MemberConverter ` は、memberがキャッシュから利用できない場合に遅延ロードでmemberを取得するようになりました。" -#: ../../whats_new.rst:492 +#: ../../whats_new.rst:525 msgid "This is the same as having ``discord.Member`` as the type-hint." msgstr "これは ``discord.Member`` を型ヒントとして使うのと同じです。" -#: ../../whats_new.rst:493 +#: ../../whats_new.rst:526 msgid ":meth:`Guild.chunk` now allows concurrent calls without spamming the gateway with requests." msgstr ":meth:`Guild.chunk` によって、Gatewayに負荷をかけずに同時呼び出しができるようになりました。" -#: ../../whats_new.rst:498 +#: ../../whats_new.rst:531 msgid "v1.5.0" msgstr "v1.5.0" -#: ../../whats_new.rst:500 +#: ../../whats_new.rst:533 msgid "This version came with forced breaking changes that Discord is requiring all bots to go through on October 7th. It is highly recommended to read the documentation on intents, :ref:`intents_primer`." msgstr "このバージョンでは、Discordが10月7日に行う、すべてのBotに要求している強制的な破壊的変更が含まれています。Intentsに関するドキュメント :ref:`intents_primer` を読むことを強くおすすめします。" -#: ../../whats_new.rst:503 +#: ../../whats_new.rst:536 msgid "API Changes" msgstr "APIの変更" -#: ../../whats_new.rst:505 +#: ../../whats_new.rst:538 msgid "Members and presences will no longer be retrieved due to an API change. See :ref:`privileged_intents` for more info." msgstr "APIの変更により、memberとpresenceの情報は取得されなくなります。 詳細は :ref:`privileged_intents` を参照してください。" -#: ../../whats_new.rst:506 +#: ../../whats_new.rst:539 msgid "As a consequence, fetching offline members is disabled if the members intent is not enabled." msgstr "結果として、 memberインテントが有効でない場合、オフラインメンバーの取得が無効になります。" -#: ../../whats_new.rst:511 +#: ../../whats_new.rst:544 msgid "Support for gateway intents, passed via ``intents`` in :class:`Client` using :class:`Intents`." msgstr ":class:`Client` において、 ``intents`` 引数に :class:`Intents` を渡すことでゲートウェイインテントがサポートされるようになりました。" -#: ../../whats_new.rst:512 +#: ../../whats_new.rst:545 msgid "Add :attr:`VoiceRegion.south_korea` (:issue:`5233`)" msgstr ":attr:`VoiceRegion.south_korea` が追加されました。 (:issue:`5233`)" -#: ../../whats_new.rst:513 +#: ../../whats_new.rst:546 msgid "Add support for ``__eq__`` for :class:`Message` (:issue:`5789`)" msgstr ":class:`Message` において、 ``__eq__`` がサポートされました。 (:issue:`5789`)" -#: ../../whats_new.rst:514 +#: ../../whats_new.rst:547 msgid "Add :meth:`Colour.dark_theme` factory method (:issue:`1584`)" msgstr ":meth:`Colour.dark_theme` クラスメソッドが追加されました。 (:issue:`1584`)" -#: ../../whats_new.rst:515 +#: ../../whats_new.rst:548 msgid "Add :meth:`AllowedMentions.none` and :meth:`AllowedMentions.all` (:issue:`5785`)" msgstr ":meth:`AllowedMentions.none` と :meth:`AllowedMentions.all` が追加されました。 (:issue:`5785`)" -#: ../../whats_new.rst:516 +#: ../../whats_new.rst:549 msgid "Add more concrete exceptions for 500 class errors under :class:`DiscordServerError` (:issue:`5797`)" msgstr ":class:`DiscordServerError` のサブクラスとして、 ステータス500エラーの具体的な例外を追加しました。 (:issue:`5797`)" -#: ../../whats_new.rst:517 +#: ../../whats_new.rst:550 msgid "Implement :class:`VoiceProtocol` to better intersect the voice flow." msgstr "音声フローをより良く交差させるため、 :class:`VoiceProtocol` を実装しました。" -#: ../../whats_new.rst:518 +#: ../../whats_new.rst:551 msgid "Add :meth:`Guild.chunk` to fully chunk a guild." msgstr "ギルドをチャンク化して取得する :meth:`Guild.chunk` を追加しました。" -#: ../../whats_new.rst:519 +#: ../../whats_new.rst:552 msgid "Add :class:`MemberCacheFlags` to better control member cache. See :ref:`intents_member_cache` for more info." msgstr "メンバーキャッシュをより適切に制御するために :class:`MemberCacheFlags` を追加しました。詳細は :ref:`intents_member_cache` を参照してください。" -#: ../../whats_new.rst:521 +#: ../../whats_new.rst:554 msgid "Add support for :attr:`ActivityType.competing` (:issue:`5823`)" msgstr ":attr:`ActivityType.competing` のサポートを追加しました。 (:issue:`5823`)" -#: ../../whats_new.rst:521 +#: ../../whats_new.rst:554 msgid "This seems currently unused API wise." msgstr "これはAPIとしては現在未使用のようです。" -#: ../../whats_new.rst:523 +#: ../../whats_new.rst:556 msgid "Add support for message references, :attr:`Message.reference` (:issue:`5754`, :issue:`5832`)" msgstr "メッセージ参照のサポート、 :attr:`Message.reference` を追加しました。 (:issue:`5754`, :issue:`5832`)" -#: ../../whats_new.rst:524 +#: ../../whats_new.rst:557 msgid "Add alias for :class:`ColourConverter` under ``ColorConverter`` (:issue:`5773`)" msgstr ":class:`ColourConverter` のエイリアス ``ColorConverter`` を追加しました。 (:issue:`5773`)" -#: ../../whats_new.rst:525 +#: ../../whats_new.rst:558 msgid "Add alias for :attr:`PublicUserFlags.verified_bot_developer` under :attr:`PublicUserFlags.early_verified_bot_developer` (:issue:`5849`)" msgstr ":attr:`PublicUserFlags.verified_bot_developer` のエイリアスを :attr:`PublicUserFlags.early_verified_bot_developer` の下に追加しました。(:issue:`5849`)" -#: ../../whats_new.rst:526 +#: ../../whats_new.rst:559 msgid "|commands| Add support for ``require_var_positional`` for :class:`Command` (:issue:`5793`)" msgstr "|commands| :class:`Command` に ``require_var_positional`` のサポートを追加しました。 (:issue:`5793`)" -#: ../../whats_new.rst:531 -#: ../../whats_new.rst:565 +#: ../../whats_new.rst:564 +#: ../../whats_new.rst:598 msgid "Fix issue with :meth:`Guild.by_category` not showing certain channels." msgstr ":meth:`Guild.by_category` がいくつかのチャンネルを表示しない問題を修正しました。" -#: ../../whats_new.rst:532 -#: ../../whats_new.rst:566 +#: ../../whats_new.rst:565 +#: ../../whats_new.rst:599 msgid "Fix :attr:`abc.GuildChannel.permissions_synced` always being ``False`` (:issue:`5772`)" msgstr ":attr:`abc.GuildChannel.permissions_synced` が常に ``False`` になる問題を修正しました。 (:issue:`5772`)" -#: ../../whats_new.rst:533 -#: ../../whats_new.rst:567 +#: ../../whats_new.rst:566 +#: ../../whats_new.rst:600 msgid "Fix handling of cloudflare bans on webhook related requests (:issue:`5221`)" msgstr "Webhook関連のリクエストでcloudflareにBANされた際の処理に発生するバグを修正しました。(:issue:`5221`)" -#: ../../whats_new.rst:534 -#: ../../whats_new.rst:568 +#: ../../whats_new.rst:567 +#: ../../whats_new.rst:601 msgid "Fix cases where a keep-alive thread would ack despite already dying (:issue:`5800`)" msgstr "キープライブスレッドが既に死んでいるにも関わらずackをするのを修正しました。(:issue:`5800`)" -#: ../../whats_new.rst:535 -#: ../../whats_new.rst:569 +#: ../../whats_new.rst:568 +#: ../../whats_new.rst:602 msgid "Fix cases where a :class:`Member` reference would be stale when cache is disabled in message events (:issue:`5819`)" msgstr "メッセージイベントでキャッシュが無効になった際に、 :class:`Member` の参照が古くなる問題を修正しました。 (:issue:`5819`)" -#: ../../whats_new.rst:536 -#: ../../whats_new.rst:570 +#: ../../whats_new.rst:569 +#: ../../whats_new.rst:603 msgid "Fix ``allowed_mentions`` not being sent when sending a single file (:issue:`5835`)" msgstr "単一のファイルを送信したときに ``allowed_mentions`` が送信されない問題を修正しました。 (:issue:`5835`)" -#: ../../whats_new.rst:537 -#: ../../whats_new.rst:571 +#: ../../whats_new.rst:570 +#: ../../whats_new.rst:604 msgid "Fix ``overwrites`` being ignored in :meth:`abc.GuildChannel.edit` if ``{}`` is passed (:issue:`5756`, :issue:`5757`)" msgstr "``{}`` が渡された場合、 :meth:`abc.GuildChannel.edit` で ``overwrites`` が無視されるのを修正しました。(:issue:`5756`, :issue:`5757`)" -#: ../../whats_new.rst:538 -#: ../../whats_new.rst:572 +#: ../../whats_new.rst:571 +#: ../../whats_new.rst:605 msgid "|commands| Fix exceptions being raised improperly in command invoke hooks (:issue:`5799`)" msgstr "|commands| コマンド呼び出しフックでの例外が正しく送出されない問題を修正しました。 (:issue:`5799`)" -#: ../../whats_new.rst:539 -#: ../../whats_new.rst:573 +#: ../../whats_new.rst:572 +#: ../../whats_new.rst:606 msgid "|commands| Fix commands not being properly ejected during errors in a cog injection (:issue:`5804`)" msgstr "|commands| コグを追加するときにエラーが発生した場合にコマンドが正しく除去されない問題を修正しました。 (:issue:`5804`)" -#: ../../whats_new.rst:540 -#: ../../whats_new.rst:574 +#: ../../whats_new.rst:573 +#: ../../whats_new.rst:607 msgid "|commands| Fix cooldown timing ignoring edited timestamps." msgstr "|commands| クールダウンのタイミングが編集のタイムスタンプを無視していたのを修正しました。" -#: ../../whats_new.rst:541 -#: ../../whats_new.rst:575 +#: ../../whats_new.rst:574 +#: ../../whats_new.rst:608 msgid "|tasks| Fix tasks extending the next iteration on handled exceptions (:issue:`5762`, :issue:`5763`)" msgstr "|tasks| 例外処理後のイテレーションでの問題を修正しました。 (:issue:`5762`, :issue:`5763`)" -#: ../../whats_new.rst:546 +#: ../../whats_new.rst:579 msgid "Webhook requests are now logged (:issue:`5798`)" msgstr "Webhookリクエストをログに記録するように変更しました。 (:issue:`5798`)" -#: ../../whats_new.rst:547 #: ../../whats_new.rst:580 +#: ../../whats_new.rst:613 msgid "Remove caching layer from :attr:`AutoShardedClient.shards`. This was causing issues if queried before launching shards." msgstr ":attr:`AutoShardedClient.shards` からキャッシュレイヤーを削除しました。これは、シャードを起動する前にクエリを実行すると問題が発生するためです。" -#: ../../whats_new.rst:548 +#: ../../whats_new.rst:581 msgid "Gateway rate limits are now handled." msgstr "ゲートウェイレート制限の処理が行われるようになりました。" -#: ../../whats_new.rst:549 +#: ../../whats_new.rst:582 msgid "Warnings logged due to missed caches are now changed to DEBUG log level." msgstr "ミスキャッシュによる警告レベルのログがDEBUGレベルのログに変更されました。" -#: ../../whats_new.rst:550 +#: ../../whats_new.rst:583 msgid "Some strings are now explicitly interned to reduce memory usage." msgstr "一部の文字列は、メモリ使用量を削減するために明示的にインターンされるようになりました。" -#: ../../whats_new.rst:551 +#: ../../whats_new.rst:584 msgid "Usage of namedtuples has been reduced to avoid potential breaking changes in the future (:issue:`5834`)" msgstr "将来的に壊れる可能性のある変更を避けるために、namedtuplesの使用が削減されました。(:issue:`5834`)" -#: ../../whats_new.rst:552 +#: ../../whats_new.rst:585 msgid "|commands| All :class:`BadArgument` exceptions from the built-in converters now raise concrete exceptions to better tell them apart (:issue:`5748`)" msgstr "|commands| ビルトインコンバータから送出されていた全ての :class:`BadArgument` 例外は、判別しやすいよう具体的な例外を発生させるようになりました。 (:issue:`5748`)" -#: ../../whats_new.rst:553 -#: ../../whats_new.rst:581 +#: ../../whats_new.rst:586 +#: ../../whats_new.rst:614 msgid "|tasks| Lazily fetch the event loop to prevent surprises when changing event loop policy (:issue:`5808`)" msgstr "|tasks| Lazily fetch event loop to prevent surprises when changing event loop policy (:issue:`5808`)" -#: ../../whats_new.rst:558 +#: ../../whats_new.rst:591 msgid "v1.4.2" msgstr "v1.4.2" -#: ../../whats_new.rst:560 +#: ../../whats_new.rst:593 msgid "This is a maintenance release with backports from :ref:`vp1p5p0`." msgstr "これは :ref:`vp1p5p0` からのバックポートによるメンテナンスリリースです。" -#: ../../whats_new.rst:586 +#: ../../whats_new.rst:619 msgid "v1.4.1" msgstr "v1.4.1" -#: ../../whats_new.rst:591 +#: ../../whats_new.rst:624 msgid "Properly terminate the connection when :meth:`Client.close` is called (:issue:`5207`)" msgstr ":meth:`Client.close` が呼び出されたときに正常に接続を終了するようにしました。 (:issue:`5207`)" -#: ../../whats_new.rst:592 +#: ../../whats_new.rst:625 msgid "Fix error being raised when clearing embed author or image when it was already cleared (:issue:`5210`, :issue:`5212`)" msgstr "埋め込みの作者や画像がすでにクリアされているときにクリアしようとするとエラーが発生するのを修正しました。 (:issue:`5210`, :issue:`5212`)" -#: ../../whats_new.rst:593 +#: ../../whats_new.rst:626 msgid "Fix ``__path__`` to allow editable extensions (:issue:`5213`)" msgstr "編集可能なエクステンションを利用できるように ``__path__`` を修正しました。 (:issue:`5213`)" -#: ../../whats_new.rst:598 +#: ../../whats_new.rst:631 msgid "v1.4.0" msgstr "v1.4.0" -#: ../../whats_new.rst:600 +#: ../../whats_new.rst:633 msgid "Another version with a long development time. Features like Intents are slated to be released in a v1.5 release. Thank you for your patience!" msgstr "長い開発時間を持つ別のバージョンです。Intentsのような機能はv1.5リリースでリリースされる予定です。ご理解いただきありがとうございます!" -#: ../../whats_new.rst:607 +#: ../../whats_new.rst:640 msgid "Add support for :class:`AllowedMentions` to have more control over what gets mentioned." msgstr "メンションの動作を制御する :class:`AllowedMentions` を追加しました。" -#: ../../whats_new.rst:606 +#: ../../whats_new.rst:639 msgid "This can be set globally through :attr:`Client.allowed_mentions`" msgstr "これは :attr:`Client.allowed_mentions` から設定することができます。" -#: ../../whats_new.rst:607 +#: ../../whats_new.rst:640 msgid "This can also be set on a per message basis via :meth:`abc.Messageable.send`" msgstr ":meth:`abc.Messageable.send` を介してメッセージごとに設定することもできます。" -#: ../../whats_new.rst:615 +#: ../../whats_new.rst:648 msgid ":class:`AutoShardedClient` has been completely redesigned from the ground up to better suit multi-process clusters (:issue:`2654`)" msgstr ":class:`AutoShardedClient` は、マルチプロセスクラスタに適した設計に完全に変更されました。(:issue:`2654`)" -#: ../../whats_new.rst:610 +#: ../../whats_new.rst:643 msgid "Add :class:`ShardInfo` which allows fetching specific information about a shard." msgstr "シャードに関する情報を取得するために :class:`ShardInfo` を追加しました。" -#: ../../whats_new.rst:611 +#: ../../whats_new.rst:644 msgid "The :class:`ShardInfo` allows for reconnecting and disconnecting of a specific shard as well." msgstr ":class:`ShardInfo` では、特定のシャードの再接続と切断も可能です。" -#: ../../whats_new.rst:612 +#: ../../whats_new.rst:645 msgid "Add :meth:`AutoShardedClient.get_shard` and :attr:`AutoShardedClient.shards` to get information about shards." msgstr "シャードに関する情報を取得するための :meth:`AutoShardedClient.get_shard` と :attr:`AutoShardedClient.shards` を追加しました。" -#: ../../whats_new.rst:613 +#: ../../whats_new.rst:646 msgid "Rework the entire connection flow to better facilitate the ``IDENTIFY`` rate limits." msgstr "接続フロー全体をリワークして、``IDENTIFY`` レート制限の対応を改善しました。" -#: ../../whats_new.rst:614 +#: ../../whats_new.rst:647 msgid "Add a hook :meth:`Client.before_identify_hook` to have better control over what happens before an ``IDENTIFY`` is done." msgstr "``IDENTIFY`` が完了する前に何を行うべきかをよりよく制御できる :meth:`Client.before_identify_hook` を追加しました。" -#: ../../whats_new.rst:615 +#: ../../whats_new.rst:648 msgid "Add more shard related events such as :func:`on_shard_connect`, :func:`on_shard_disconnect` and :func:`on_shard_resumed`." msgstr ":func:`on_shard_connect` 、 :func:`on_shard_disconnect` 、 :func:`on_shard_resumed` などのシャード関連イベントを追加しました。" -#: ../../whats_new.rst:621 +#: ../../whats_new.rst:654 msgid "Add support for guild templates (:issue:`2652`)" msgstr "サーバーテンプレートのサポートを追加しました。 (:issue:`2652`)" -#: ../../whats_new.rst:618 +#: ../../whats_new.rst:651 msgid "This adds :class:`Template` to read a template's information." msgstr "テンプレートの情報を読むために :class:`Template` を追加しました。" -#: ../../whats_new.rst:619 +#: ../../whats_new.rst:652 msgid ":meth:`Client.fetch_template` can be used to fetch a template's information from the API." msgstr "テンプレートの情報を API から取得するには :meth:`Client.fetch_template` が使用できます。" -#: ../../whats_new.rst:620 +#: ../../whats_new.rst:653 msgid ":meth:`Client.create_guild` can now take an optional template to base the creation from." msgstr ":meth:`Client.create_guild` は任意で作成元のテンプレートを取ることができます。" -#: ../../whats_new.rst:621 +#: ../../whats_new.rst:654 msgid "Note that fetching a guild's template is currently restricted for bot accounts." msgstr "Botアカウントでは、ギルドのテンプレートの取得は現在制限されていることに注意してください。" -#: ../../whats_new.rst:631 +#: ../../whats_new.rst:664 msgid "Add support for guild integrations (:issue:`2051`, :issue:`1083`)" msgstr "ギルドインテグレーションのサポートを追加しました。 (:issue:`2051`, :issue:`1083`)" -#: ../../whats_new.rst:624 +#: ../../whats_new.rst:657 msgid ":class:`Integration` is used to read integration information." msgstr ":class:`Integration` はインテグレーション情報の読み取りに使用されます。" -#: ../../whats_new.rst:625 +#: ../../whats_new.rst:658 msgid ":class:`IntegrationAccount` is used to read integration account information." msgstr ":class:`IntegrationAccount` はインテグレーションアカウント情報の読み取りに使用されます。" -#: ../../whats_new.rst:626 +#: ../../whats_new.rst:659 msgid ":meth:`Guild.integrations` will fetch all integrations in a guild." msgstr ":meth:`Guild.integrations` はギルド内の全てのインテグレーションを取得します。" -#: ../../whats_new.rst:627 +#: ../../whats_new.rst:660 msgid ":meth:`Guild.create_integration` will create an integration." msgstr ":meth:`Guild.create_integration` はインテグレーションを作成します。" -#: ../../whats_new.rst:628 +#: ../../whats_new.rst:661 msgid ":meth:`Integration.edit` will edit an existing integration." msgstr ":meth:`Integration.edit` は既存のインテグレーションを編集します。" -#: ../../whats_new.rst:629 +#: ../../whats_new.rst:662 msgid ":meth:`Integration.delete` will delete an integration." msgstr ":meth:`Integration.delete` はインテグレーションを削除します。" -#: ../../whats_new.rst:630 +#: ../../whats_new.rst:663 msgid ":meth:`Integration.sync` will sync an integration." msgstr ":meth:`Integration.sync` はインテグレーションを同期します。" -#: ../../whats_new.rst:631 +#: ../../whats_new.rst:664 msgid "There is currently no support in the audit log for this." msgstr "これには現時点で監査ログのサポートはありません。" -#: ../../whats_new.rst:633 +#: ../../whats_new.rst:666 msgid "Add an alias for :attr:`VerificationLevel.extreme` under :attr:`VerificationLevel.very_high` (:issue:`2650`)" msgstr ":attr:`VerificationLevel.extreme` の別名を :attr:`VerificationLevel.very_high` の下に追加しました (:issue:`2650`)" -#: ../../whats_new.rst:634 +#: ../../whats_new.rst:667 msgid "Add various grey to gray aliases for :class:`Colour` (:issue:`5130`)" msgstr ":class:`Colour` に「グレー」の綴り違いのエイリアスを追加しました。 (:issue:`5130`)" -#: ../../whats_new.rst:635 +#: ../../whats_new.rst:668 msgid "Added :attr:`VoiceClient.latency` and :attr:`VoiceClient.average_latency` (:issue:`2535`)" msgstr ":attr:`VoiceClient.latency` と :attr:`VoiceClient.average_latency` を追加しました。 (:issue:`2535`)" -#: ../../whats_new.rst:636 +#: ../../whats_new.rst:669 msgid "Add ``use_cached`` and ``spoiler`` parameters to :meth:`Attachment.to_file` (:issue:`2577`, :issue:`4095`)" msgstr ":meth:`Attachment.to_file` にパラメータ ``use_cached`` と ``spoiler`` を追加しました。 (:issue:`2577`, :issue:`4095`)" -#: ../../whats_new.rst:637 +#: ../../whats_new.rst:670 msgid "Add ``position`` parameter support to :meth:`Guild.create_category` (:issue:`2623`)" msgstr ":meth:`Guild.create_category` にて ``position`` パラメータのサポートを追加しました。 (:issue:`2623`)" -#: ../../whats_new.rst:638 +#: ../../whats_new.rst:671 msgid "Allow passing ``int`` for the colour in :meth:`Role.edit` (:issue:`4057`)" msgstr ":meth:`Role.edit` のロールカラーに ``int`` が渡せるようになりました。 (:issue:`4057`)" -#: ../../whats_new.rst:639 +#: ../../whats_new.rst:672 msgid "Add :meth:`Embed.remove_author` to clear author information from an embed (:issue:`4068`)" msgstr "埋め込みの作者を削除する :meth:`Embed.remove_author` が追加されました。 (:issue:`4068`)" -#: ../../whats_new.rst:640 +#: ../../whats_new.rst:673 msgid "Add the ability to clear images and thumbnails in embeds using :attr:`Embed.Empty` (:issue:`4053`)" msgstr ":attr:`Embed.Empty` を使用してEmbed内のサムネイルと画像をクリアできるようになりました。 (:issue:`4053`)" -#: ../../whats_new.rst:641 +#: ../../whats_new.rst:674 msgid "Add :attr:`Guild.max_video_channel_users` (:issue:`4120`)" msgstr ":attr:`Guild.max_video_channel_users` を追加。( :issue:`4120` )" -#: ../../whats_new.rst:642 +#: ../../whats_new.rst:675 msgid "Add :attr:`Guild.public_updates_channel` (:issue:`4120`)" msgstr ":attr:`Guild.public_updates_channel` を追加。( :issue:`4120` )" -#: ../../whats_new.rst:643 +#: ../../whats_new.rst:676 msgid "Add ``guild_ready_timeout`` parameter to :class:`Client` and subclasses to control timeouts when the ``GUILD_CREATE`` stream takes too long (:issue:`4112`)" msgstr "``GUILD_CREATE`` に時間がかかりすぎるとき、タイムアウトをコントロールできように ``guild_ready_timeout`` パラメータを :class:`Client` に追加しました。 (:issue:`4112`)" -#: ../../whats_new.rst:644 +#: ../../whats_new.rst:677 msgid "Add support for public user flags via :attr:`User.public_flags` and :class:`PublicUserFlags` (:issue:`3999`)" msgstr ":attr:`User.public_flags` と :class:`PublicUserFlags` を介しユーザーフラグのサポートを追加しました。 (:issue:`3999`)" -#: ../../whats_new.rst:645 +#: ../../whats_new.rst:678 msgid "Allow changing of channel types via :meth:`TextChannel.edit` to and from a news channel (:issue:`4121`)" msgstr ":meth:`TextChannel.edit` を介してニュースチャンネルの種類を変更することができるようにしました。(:issue:`4121` )" -#: ../../whats_new.rst:646 +#: ../../whats_new.rst:679 msgid "Add :meth:`Guild.edit_role_positions` to bulk edit role positions in a single API call (:issue:`2501`, :issue:`2143`)" msgstr "一回のAPI呼び出しでロールの位置を一括変更できる :meth:`Guild.edit_role_positions` を追加しました。 (:issue:`2501`, :issue:`2143`)" -#: ../../whats_new.rst:647 +#: ../../whats_new.rst:680 msgid "Add :meth:`Guild.change_voice_state` to change your voice state in a guild (:issue:`5088`)" msgstr "ギルド内のボイスステートを変更する :meth:`Guild.change_voice_state` を追加しました。 (:issue:`5088`)" -#: ../../whats_new.rst:648 +#: ../../whats_new.rst:681 msgid "Add :meth:`PartialInviteGuild.is_icon_animated` for checking if the invite guild has animated icon (:issue:`4180`, :issue:`4181`)" msgstr "ギルドにアニメーションアイコンがあるか判断する :meth:`PartialInviteGuild.is_icon_animated` を追加しました。 (:issue:`4180`, :issue:`4181`)" -#: ../../whats_new.rst:649 +#: ../../whats_new.rst:682 msgid "Add :meth:`PartialInviteGuild.icon_url_as` now supports ``static_format`` for consistency (:issue:`4180`, :issue:`4181`)" msgstr "``static_format`` が :meth:`PartialInviteGuild.icon_url_as` に追加されました (:issue:`4180`, :issue:`4181`)" -#: ../../whats_new.rst:650 +#: ../../whats_new.rst:683 msgid "Add support for ``user_ids`` in :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` で、 ``user_ids`` 引数が使えるようになりました。" -#: ../../whats_new.rst:651 +#: ../../whats_new.rst:684 msgid "Add support for pruning members by roles in :meth:`Guild.prune_members` (:issue:`4043`)" msgstr ":meth:`Guild.prune_members` でメンバーをロールにより一括キックできるようになりました。 (:issue:`4043`)" -#: ../../whats_new.rst:652 +#: ../../whats_new.rst:685 msgid "|commands| Implement :func:`~ext.commands.before_invoke` and :func:`~ext.commands.after_invoke` decorators (:issue:`1986`, :issue:`2502`)" msgstr "|commands| :func:`~ext.commands.before_invoke` と :func:`~ext.commands.after_invoke` デコレーターを実装。 ( :issue:`1986`, :issue:`2502` )" -#: ../../whats_new.rst:653 +#: ../../whats_new.rst:686 msgid "|commands| Add a way to retrieve ``retry_after`` from a cooldown in a command via :meth:`Command.get_cooldown_retry_after <.ext.commands.Command.get_cooldown_retry_after>` (:issue:`5195`)" msgstr "|commands| :meth:`Command.get_cooldown_retry_after <.ext.commands.Command.get_cooldown_retry_after>` によってコマンド中のクールダウンから ``retry_after`` を取得する方法を追加しました (:issue:`5195`)" -#: ../../whats_new.rst:654 +#: ../../whats_new.rst:687 msgid "|commands| Add a way to dynamically add and remove checks from a :class:`HelpCommand <.ext.commands.HelpCommand>` (:issue:`5197`)" msgstr "|commands| :class:`HelpCommand <.ext.commands.HelpCommand>` から動的にチェックを追加したり削除したりする方法を追加しました (:issue:`5197`)" -#: ../../whats_new.rst:655 +#: ../../whats_new.rst:688 msgid "|tasks| Add :meth:`Loop.is_running <.ext.tasks.Loop.is_running>` method to the task objects (:issue:`2540`)" msgstr "|tasks| タスクオブジェクトに :meth:`Loop.is_running <.ext.tasks.Loop.is_running>` メソッドを追加しました (:issue:`2540`)" -#: ../../whats_new.rst:656 +#: ../../whats_new.rst:689 msgid "|tasks| Allow usage of custom error handlers similar to the command extensions to tasks using :meth:`Loop.error <.ext.tasks.Loop.error>` decorator (:issue:`2621`)" msgstr "|tasks| :meth:`Loop.error <.ext.tasks.Loop.error>` デコレーターを用いたタスクに対するコマンド拡張と同様のカスタムエラーハンドラーの使用を可能にしました (:issue:`2621`)" -#: ../../whats_new.rst:662 +#: ../../whats_new.rst:695 msgid "Fix issue with :attr:`PartialEmoji.url` reads leading to a failure (:issue:`4015`, :issue:`4016`)" msgstr ":attr:`PartialEmoji.url` での読み込みエラーを修正しました。 (:issue:`4015`, :issue:`4016`)" -#: ../../whats_new.rst:663 +#: ../../whats_new.rst:696 msgid "Allow :meth:`abc.Messageable.history` to take a limit of ``1`` even if ``around`` is passed (:issue:`4019`)" msgstr "``around`` が渡された場合でも、 :meth:`abc.Messageable.history` が上限 ``1`` を取ることができるようにしました。 (:issue:`4019`)" -#: ../../whats_new.rst:664 +#: ../../whats_new.rst:697 msgid "Fix :attr:`Guild.member_count` not updating in certain cases when a member has left the guild (:issue:`4021`)" msgstr "ギルドからメンバーが脱退したとき、特定の場合に :attr:`Guild.member_count` が更新されない問題を修正しました。 (:issue:`4021`)" -#: ../../whats_new.rst:665 +#: ../../whats_new.rst:698 msgid "Fix the type of :attr:`Object.id` not being validated. For backwards compatibility ``str`` is still allowed but is converted to ``int`` (:issue:`4002`)" msgstr ":attr:`Object.id` の型が検証されない問題を修正されました。後方互換性のため ``str`` は使用可能ですが、 ``int`` に変換されます。 (:issue:`4002`)" -#: ../../whats_new.rst:666 +#: ../../whats_new.rst:699 msgid "Fix :meth:`Guild.edit` not allowing editing of notification settings (:issue:`4074`, :issue:`4047`)" msgstr ":meth:`Guild.edit` で通知設定の編集ができない問題を修正しました。 (:issue:`4074`, :issue:`4047`)" -#: ../../whats_new.rst:667 +#: ../../whats_new.rst:700 msgid "Fix crash when the guild widget contains channels that aren't in the payload (:issue:`4114`, :issue:`4115`)" msgstr "ギルドウィジェットの中にペイロードにないチャンネルが含まれている場合にクラッシュする問題を修正しました。 (:issue:`4114`, :issue:`4115`)" -#: ../../whats_new.rst:668 +#: ../../whats_new.rst:701 msgid "Close ffmpeg stdin handling from spawned processes with :class:`FFmpegOpusAudio` and :class:`FFmpegPCMAudio` (:issue:`4036`)" msgstr ":class:`FFmpegOpusAudio` および :class:`FFmpegPCMAudio` を使って生成されたプロセスからの ffmpeg stdin のハンドリングを閉じるようにしました (:issue:`4036`)" -#: ../../whats_new.rst:669 +#: ../../whats_new.rst:702 msgid "Fix :func:`utils.escape_markdown` not escaping masked links (:issue:`4206`, :issue:`4207`)" msgstr ":func:`utils.escape_markdown` がマスクされたリンクをエスケープしない問題を修正しました。 (:issue:`4206`, :issue:`4207`)" -#: ../../whats_new.rst:670 +#: ../../whats_new.rst:703 msgid "Fix reconnect loop due to failed handshake on region change (:issue:`4210`, :issue:`3996`)" msgstr "リージョン変更時のハンドシェイクの失敗による再接続のループを修正しました (:issue:`4210`, :issue:`3996`)" -#: ../../whats_new.rst:671 +#: ../../whats_new.rst:704 msgid "Fix :meth:`Guild.by_category` not returning empty categories (:issue:`4186`)" msgstr "空のカテゴリーを返さない :meth:`Guild.by_category` を修正しました (:issue:`4186`)" -#: ../../whats_new.rst:672 +#: ../../whats_new.rst:705 msgid "Fix certain JPEG images not being identified as JPEG (:issue:`5143`)" msgstr "特定の JPEG 画像が JPEG として認識されないのを修正 (:issue:`5143`)" -#: ../../whats_new.rst:673 +#: ../../whats_new.rst:706 msgid "Fix a crash when an incomplete guild object is used when fetching reaction information (:issue:`5181`)" msgstr "反応情報を取得する際に不完全なギルドオブジェクトを使用するとクラッシュする問題を修正しました (:issue:`5181`)" -#: ../../whats_new.rst:674 +#: ../../whats_new.rst:707 msgid "Fix a timeout issue when fetching members using :meth:`Guild.query_members`" msgstr ":meth:`Guild.query_members` を使用してメンバーを取得する際のタイムアウトの問題を修正しました。" -#: ../../whats_new.rst:675 +#: ../../whats_new.rst:708 msgid "Fix an issue with domain resolution in voice (:issue:`5188`, :issue:`5191`)" msgstr "音声のドメイン解決に関する問題を修正しました (:issue:`5188`, :issue:`5191`)" -#: ../../whats_new.rst:676 +#: ../../whats_new.rst:709 msgid "Fix an issue where :attr:`PartialEmoji.id` could be a string (:issue:`4153`, :issue:`4152`)" msgstr ":attr:`PartialEmoji.id` が文字列である可能性がある問題を修正しました (:issue:`4153`, :issue:`4152`)" -#: ../../whats_new.rst:677 +#: ../../whats_new.rst:710 msgid "Fix regression where :attr:`Member.activities` would not clear." msgstr ":attr:`Member.activities` がクリアされないリグレッションを修正しました。" -#: ../../whats_new.rst:678 +#: ../../whats_new.rst:711 msgid "|commands| A :exc:`TypeError` is now raised when :obj:`typing.Optional` is used within :data:`commands.Greedy <.ext.commands.Greedy>` (:issue:`2253`, :issue:`5068`)" msgstr "|commands| :data:`commands.Greedy <.ext.commands.Greedy>` 内で :obj:`typing.Optional` を使用すると :exc:`TypeError` が発生します (:issue:`2253`, :issue:`5068`)." -#: ../../whats_new.rst:679 +#: ../../whats_new.rst:712 msgid "|commands| :meth:`Bot.walk_commands <.ext.commands.Bot.walk_commands>` no longer yields duplicate commands due to aliases (:issue:`2591`)" msgstr "|commands| :meth:`Bot.walk_commands <.ext.commands.Bot.walk_commands>` はエイリアスにより重複したコマンドを生成しないようになりました (:issue:`2591`)" -#: ../../whats_new.rst:680 +#: ../../whats_new.rst:713 msgid "|commands| Fix regex characters not being escaped in :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` (:issue:`4058`, :issue:`4071`)" msgstr "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` で正規化されていない文字を修正しました (:issue:`4058`, :issue:`4071`)" -#: ../../whats_new.rst:681 +#: ../../whats_new.rst:714 msgid "|commands| Fix :meth:`Bot.get_command <.ext.commands.Bot.get_command>` from raising errors when a name only has whitespace (:issue:`5124`)" msgstr "|commands| 名前に空白文字しかない場合にエラーを発生させないように :meth:`Bot.get_command <.ext.commands.Bot.get_command>` を修正しました (:issue:`5124`)" -#: ../../whats_new.rst:682 +#: ../../whats_new.rst:715 msgid "|commands| Fix issue with :attr:`Context.subcommand_passed <.ext.commands.Context.subcommand_passed>` not functioning as expected (:issue:`5198`)" msgstr "|commands| :attr:`Context.subcommand_passed <.ext.commands.Context.subcommand_passed>` が期待通りに機能しない問題を修正しました (:issue:`5198`)" -#: ../../whats_new.rst:683 +#: ../../whats_new.rst:716 msgid "|tasks| Task objects are no longer stored globally so two class instances can now start two separate tasks (:issue:`2294`)" msgstr "|tasks| Task objects are no longer stored globally so two class instances can start two separate tasks (:issue:`2294`)" -#: ../../whats_new.rst:684 +#: ../../whats_new.rst:717 msgid "|tasks| Allow cancelling the loop within :meth:`before_loop <.ext.tasks.Loop.before_loop>` (:issue:`4082`)" msgstr "|tasks| 内のループをキャンセルできるようにする。:meth:`before_loop <.ext.tasks.Loop.before_loop>` (:issue:`4082`)" -#: ../../whats_new.rst:690 +#: ../../whats_new.rst:723 msgid "The :attr:`Member.roles` cache introduced in v1.3 was reverted due to issues caused (:issue:`4087`, :issue:`4157`)" msgstr "v1.3 で導入された :attr:`Member.roles` キャッシュは、問題が発生したため元に戻されました (:issue:`4087`, :issue:`4157`)" -#: ../../whats_new.rst:691 +#: ../../whats_new.rst:724 msgid ":class:`Webhook` objects are now comparable and hashable (:issue:`4182`)" msgstr ":class:`Webhook` オブジェクトが比較可能になり、ハッシュ化できるようになりました (:issue:`4182`)" -#: ../../whats_new.rst:695 +#: ../../whats_new.rst:728 msgid "Some more API requests got a ``reason`` parameter for audit logs (:issue:`5086`)" msgstr "さらにいくつかの API リクエストで、監査ログ用の ``reason`` パラメータが取得されました (:issue:`5086`)" -#: ../../whats_new.rst:693 +#: ../../whats_new.rst:726 msgid ":meth:`TextChannel.follow`" msgstr ":meth:`TextChannel.follow`" -#: ../../whats_new.rst:694 +#: ../../whats_new.rst:727 msgid ":meth:`Message.pin` and :meth:`Message.unpin`" msgstr ":meth:`Message.pin` と :meth:`Message.unpin`" -#: ../../whats_new.rst:695 +#: ../../whats_new.rst:728 msgid ":meth:`Webhook.delete` and :meth:`Webhook.edit`" msgstr ":meth:`Webhook.delete` と :meth:`Webhook.edit`" -#: ../../whats_new.rst:697 +#: ../../whats_new.rst:730 msgid "For performance reasons ``websockets`` has been dropped in favour of ``aiohttp.ws``." msgstr "パフォーマンス上の理由から、 ``websockets`` は削除され、 ``aiohttp.ws`` が使用されるようになりました。" -#: ../../whats_new.rst:698 +#: ../../whats_new.rst:731 msgid "The blocking logging message now shows the stack trace of where the main thread was blocking" msgstr "ブロッキングのログメッセージは、メインスレッドがブロッキングしていた場所のスタックトレースを表示するようになりました" -#: ../../whats_new.rst:699 +#: ../../whats_new.rst:732 msgid "The domain name was changed from ``discordapp.com`` to ``discord.com`` to prepare for the required domain migration" msgstr "必要なドメイン移行の準備のため、ドメイン名を ``discordapp.com`` から ``discord.com`` に変更しました。" -#: ../../whats_new.rst:700 +#: ../../whats_new.rst:733 msgid "Reduce memory usage when reconnecting due to stale references being held by the message cache (:issue:`5133`)" msgstr "メッセージキャッシュに保持されている古い参照による再接続時のメモリ使用量を削減しました (:issue:`5133`)" -#: ../../whats_new.rst:701 +#: ../../whats_new.rst:734 msgid "Optimize :meth:`abc.GuildChannel.permissions_for` by not creating as many temporary objects (20-32% savings)." msgstr "テンポラリオブジェクトをあまり作成しないように :meth:`abc.GuildChannel.permissions_for` を最適化しました (20-32%の節約)。" -#: ../../whats_new.rst:702 +#: ../../whats_new.rst:735 msgid "|commands| Raise :exc:`~ext.commands.CommandRegistrationError` instead of :exc:`ClientException` when a duplicate error is registered (:issue:`4217`)" msgstr "|commands| 重複するエラーが登録された場合、 :exc:`ClientException` ではなく :exc:`~ext.commands.CommandRegistrationError` を発生するようにしました (:issue:`4217`)" -#: ../../whats_new.rst:703 +#: ../../whats_new.rst:736 msgid "|tasks| No longer handle :exc:`HTTPException` by default in the task reconnect loop (:issue:`5193`)" msgstr "|tasks| タスクの再接続ループにおいて、デフォルトで :exc:`HTTPException` を処理しないようにしました (:issue:`5193`)" -#: ../../whats_new.rst:708 +#: ../../whats_new.rst:741 msgid "v1.3.4" msgstr "v1.3.4" -#: ../../whats_new.rst:713 +#: ../../whats_new.rst:746 msgid "Fix an issue with channel overwrites causing multiple issues including crashes (:issue:`5109`)" msgstr "チャンネルの上書きがクラッシュを含む複数の問題を引き起こす問題を修正しました (:issue:`5109`)" -#: ../../whats_new.rst:718 +#: ../../whats_new.rst:751 msgid "v1.3.3" msgstr "v1.3.3" -#: ../../whats_new.rst:724 +#: ../../whats_new.rst:757 msgid "Change default WS close to 4000 instead of 1000." msgstr "デフォルトのWSクローズを1000から4000に変更。" -#: ../../whats_new.rst:724 +#: ../../whats_new.rst:757 msgid "The previous close code caused sessions to be invalidated at a higher frequency than desired." msgstr "以前のクローズコードは、望ましい頻度よりも高い頻度でセッションが無効化される原因となっていました。" -#: ../../whats_new.rst:726 +#: ../../whats_new.rst:759 msgid "Fix ``None`` appearing in ``Member.activities``. (:issue:`2619`)" msgstr "``Member.activities`` に表示される ``None`` を修正しました。(:issue:`2619`)" -#: ../../whats_new.rst:731 +#: ../../whats_new.rst:764 msgid "v1.3.2" msgstr "v1.3.2" -#: ../../whats_new.rst:733 +#: ../../whats_new.rst:766 msgid "Another minor bug fix release." msgstr "もう一つのマイナーなバグフィックスリリースです。" -#: ../../whats_new.rst:738 +#: ../../whats_new.rst:771 msgid "Higher the wait time during the ``GUILD_CREATE`` stream before ``on_ready`` is fired for :class:`AutoShardedClient`." msgstr ":class:`AutoShardedClient` の ``GUILD_CREATE`` ストリームで ``on_ready`` が発生するまでの待ち時間を長くするようにしました。" -#: ../../whats_new.rst:739 +#: ../../whats_new.rst:772 msgid ":func:`on_voice_state_update` now uses the inner ``member`` payload which should make it more reliable." msgstr ":func:`on_voice_state_update` は内側の ``member`` ペイロードを使用するようになり、より信頼性が高くなりました。" -#: ../../whats_new.rst:740 +#: ../../whats_new.rst:773 msgid "Fix various Cloudflare handling errors (:issue:`2572`, :issue:`2544`)" msgstr "Cloudflare のハンドリングエラーを修正しました (:issue:`2572`, :issue:`2544`)" -#: ../../whats_new.rst:741 +#: ../../whats_new.rst:774 msgid "Fix crashes if :attr:`Message.guild` is :class:`Object` instead of :class:`Guild`." msgstr ":attr:`Message.guild` が :class:`Guild` ではなく :class:`Object` であった場合のクラッシュを修正しました。" -#: ../../whats_new.rst:742 +#: ../../whats_new.rst:775 msgid "Fix :meth:`Webhook.send` returning an empty string instead of ``None`` when ``wait=False``." msgstr ":meth:`Webhook.send` が ``wait=False`` の時に ``None`` ではなく空の文字列を返すように修正しました。" -#: ../../whats_new.rst:743 +#: ../../whats_new.rst:776 msgid "Fix invalid format specifier in webhook state (:issue:`2570`)" msgstr "Webhook の状態における無効なフォーマット指定子を修正 (:issue:`2570`)" -#: ../../whats_new.rst:744 +#: ../../whats_new.rst:777 msgid "|commands| Passing invalid permissions to permission related checks now raises ``TypeError``." msgstr "|commands| パーミッション関連のチェックで無効なパーミッションを渡すと ``TypeError`` が発生するようになりました。" -#: ../../whats_new.rst:749 +#: ../../whats_new.rst:782 msgid "v1.3.1" msgstr "v1.3.1" -#: ../../whats_new.rst:751 +#: ../../whats_new.rst:784 msgid "Minor bug fix release." msgstr "マイナーなバグフィックスリリースです。" -#: ../../whats_new.rst:756 +#: ../../whats_new.rst:789 msgid "Fix fetching invites in guilds that the user is not in." msgstr "ユーザーが参加していないギルドの招待状をフェッチするように修正しました。" -#: ../../whats_new.rst:757 +#: ../../whats_new.rst:790 msgid "Fix the channel returned from :meth:`Client.fetch_channel` raising when sending messages. (:issue:`2531`)" msgstr "メッセージ送信時に :meth:`Client.fetch_channel` から返されるチャンネルを修正しました。(:issue:`2531`)" -#: ../../whats_new.rst:762 +#: ../../whats_new.rst:795 msgid "Fix compatibility warnings when using the Python 3.9 alpha." msgstr "Python 3.9 alpha を使用する際の互換性警告を修正。" -#: ../../whats_new.rst:763 +#: ../../whats_new.rst:796 msgid "Change the unknown event logging from WARNING to DEBUG to reduce noise." msgstr "ノイズを減らすために、不明なイベントのログをWARNINGからDEBUGに変更します。" -#: ../../whats_new.rst:768 +#: ../../whats_new.rst:801 msgid "v1.3.0" msgstr "v1.3.0" -#: ../../whats_new.rst:770 +#: ../../whats_new.rst:803 msgid "This version comes with a lot of bug fixes and new features. It's been in development for a lot longer than was anticipated!" msgstr "このバージョンでは、多くのバグフィックスと新機能が搭載されています。予想以上に長い期間、開発が続けられているのです!" -#: ../../whats_new.rst:775 +#: ../../whats_new.rst:808 msgid "Add :meth:`Guild.fetch_members` to fetch members from the HTTP API. (:issue:`2204`)" msgstr "HTTP API からメンバーを取得するための :meth:`Guild.fetch_members` を追加しました。(:issue:`2204`)" -#: ../../whats_new.rst:776 +#: ../../whats_new.rst:809 msgid "Add :meth:`Guild.fetch_roles` to fetch roles from the HTTP API. (:issue:`2208`)" msgstr "HTTP API からロールをフェッチするために :meth:`Guild.fetch_roles` を追加しました。(:issue:`2208`)" -#: ../../whats_new.rst:777 +#: ../../whats_new.rst:810 msgid "Add support for teams via :class:`Team` when fetching with :meth:`Client.application_info`. (:issue:`2239`)" msgstr ":meth:`Client.application_info` で取得する際に、 :class:`Team` を介してチームをサポートする機能を追加しました。(:issue:`2239`)" -#: ../../whats_new.rst:778 +#: ../../whats_new.rst:811 msgid "Add support for suppressing embeds via :meth:`Message.edit`" msgstr ":meth:`Message.edit` による埋め込みの抑制をサポートするようにしました。" -#: ../../whats_new.rst:779 +#: ../../whats_new.rst:812 msgid "Add support for guild subscriptions. See the :class:`Client` documentation for more details." msgstr "ギルドサブスクリプションのサポートを追加しました。詳細は :class:`Client` のドキュメントを参照してください。" -#: ../../whats_new.rst:780 +#: ../../whats_new.rst:813 msgid "Add :attr:`VoiceChannel.voice_states` to get voice states without relying on member cache." msgstr "メンバーキャッシュに依存せずに音声の状態を取得するために、 :attr:`VoiceChannel.voice_states` を追加しました。" -#: ../../whats_new.rst:781 +#: ../../whats_new.rst:814 msgid "Add :meth:`Guild.query_members` to request members from the gateway." msgstr "ゲートウェイにメンバーを要求するために :meth:`Guild.query_members` を追加しました。" -#: ../../whats_new.rst:782 +#: ../../whats_new.rst:815 msgid "Add :class:`FFmpegOpusAudio` and other voice improvements. (:issue:`2258`)" msgstr ":class:`FFmpegOpusAudio` を追加し、その他の音声の改良を行いました。(:issue:`2258`)" -#: ../../whats_new.rst:783 +#: ../../whats_new.rst:816 msgid "Add :attr:`RawMessageUpdateEvent.channel_id` for retrieving channel IDs during raw message updates. (:issue:`2301`)" msgstr "Rawメッセージの更新時にチャンネルIDを取得するための :attr:`RawMessageUpdateEvent.channel_id` を追加しました。(:issue:`2301`)" -#: ../../whats_new.rst:784 +#: ../../whats_new.rst:817 msgid "Add :attr:`RawReactionActionEvent.event_type` to disambiguate between reaction addition and removal in reaction events." msgstr "リアクションイベントでリアクションが追加されたか除去されたかを明確にする :attr:`RawReactionActionEvent.event_type` を追加しました。" -#: ../../whats_new.rst:785 +#: ../../whats_new.rst:818 msgid "Add :attr:`abc.GuildChannel.permissions_synced` to query whether permissions are synced with the category. (:issue:`2300`, :issue:`2324`)" msgstr "権限がカテゴリと同期されているかを確認する :attr:`abc.GuildChannel.permissions_synced` を追加しました。 (:issue:`2300`, :issue:`2324`)" -#: ../../whats_new.rst:786 +#: ../../whats_new.rst:819 msgid "Add :attr:`MessageType.channel_follow_add` message type for announcement channels being followed. (:issue:`2314`)" msgstr "フォローされているアナウンスチャンネル用の :attr:`MessageType.channel_follow_add` メッセージタイプを追加しました。(:issue:`2314`)" -#: ../../whats_new.rst:787 +#: ../../whats_new.rst:820 msgid "Add :meth:`Message.is_system` to allow for quickly filtering through system messages." msgstr "システムメッセージを素早くフィルタリングできるように :meth:`Message.is_system` を追加しました。" -#: ../../whats_new.rst:788 +#: ../../whats_new.rst:821 msgid "Add :attr:`VoiceState.self_stream` to indicate whether someone is streaming via Go Live. (:issue:`2343`)" msgstr "誰かがGo Live経由でストリーミングしているかどうかを示すための、 :attr:`VoiceState.self_stream` を追加しました。 (:issue:`2343`)" -#: ../../whats_new.rst:789 +#: ../../whats_new.rst:822 msgid "Add :meth:`Emoji.is_usable` to check if the client user can use an emoji. (:issue:`2349`)" msgstr "クライアントユーザーが絵文字を使用できるかどうかを確認できるように、 :meth:`Emoji.is_usable` を追加しました。 (:issue:`2349`)" -#: ../../whats_new.rst:790 +#: ../../whats_new.rst:823 msgid "Add :attr:`VoiceRegion.europe` and :attr:`VoiceRegion.dubai`. (:issue:`2358`, :issue:`2490`)" msgstr ":attr:`VoiceRegion.europe` と :attr:`VoiceRegion.dubai` を追加しました。 (:issue:`2358`, :issue:`2490`)" -#: ../../whats_new.rst:791 +#: ../../whats_new.rst:824 msgid "Add :meth:`TextChannel.follow` to follow a news channel. (:issue:`2367`)" msgstr "ニュースチャンネルをフォローする :meth:`TextChannel.follow` を追加しました。 (:issue:`2367`)" -#: ../../whats_new.rst:792 +#: ../../whats_new.rst:825 msgid "Add :attr:`Permissions.view_guild_insights` permission. (:issue:`2415`)" msgstr ":attr:`Permissions.view_guild_insights` 権限を追加しました。 (:issue:`2415`)" -#: ../../whats_new.rst:794 +#: ../../whats_new.rst:827 msgid "Add support for new audit log types. See :ref:`discord-api-audit-logs` for more information. (:issue:`2427`)" msgstr "新しい監査ログタイプのサポートを追加しました。詳細については :ref:`discord-api-audit-logs` を参照してください。 (:issue:`2427`)" -#: ../../whats_new.rst:794 +#: ../../whats_new.rst:827 msgid "Note that integration support is not finalized." msgstr "インテグレーションのサポートは未確定であることに注意してください。" -#: ../../whats_new.rst:796 +#: ../../whats_new.rst:829 msgid "Add :attr:`Webhook.type` to query the type of webhook (:class:`WebhookType`). (:issue:`2441`)" msgstr "ウェブフック( :class:`WebhookType` )の種類を問い合わせるための :attr:`Webhook.type` を追加しました。 (:issue:`2441`)" -#: ../../whats_new.rst:797 +#: ../../whats_new.rst:830 msgid "Allow bulk editing of channel overwrites through :meth:`abc.GuildChannel.edit`. (:issue:`2198`)" msgstr "チャンネル上書きの一括編集を :meth:`abc.GuildChannel.edit` を通して行えるようにしました。(:issue:`2198`)" -#: ../../whats_new.rst:798 +#: ../../whats_new.rst:831 msgid "Add :class:`Activity.created_at` to see when an activity was started. (:issue:`2446`)" msgstr "アクティビティがいつ開始されたかを確認するために :class:`Activity.created_at` を追加しました。(:issue:`2446`)" -#: ../../whats_new.rst:799 +#: ../../whats_new.rst:832 msgid "Add support for ``xsalsa20_poly1305_lite`` encryption mode for voice. (:issue:`2463`)" msgstr "音声用の ``xsalsa20_poly1305_lite`` 暗号化モードのサポートを追加しました。(:issue:`2463`)" -#: ../../whats_new.rst:800 +#: ../../whats_new.rst:833 msgid "Add :attr:`RawReactionActionEvent.member` to get the member who did the reaction. (:issue:`2443`)" msgstr "リアクションを行ったメンバーを取得するために :attr:`RawReactionActionEvent.member` を追加しました。(:issue:`2443`)" -#: ../../whats_new.rst:801 +#: ../../whats_new.rst:834 msgid "Add support for new YouTube streaming via :attr:`Streaming.platform` and :attr:`Streaming.game`. (:issue:`2445`)" msgstr ":attr:`Streaming.platform` と :attr:`Streaming.game` による新しい YouTube ストリーミングのサポートを追加しました。(:issue:`2445`)" -#: ../../whats_new.rst:802 +#: ../../whats_new.rst:835 msgid "Add :attr:`Guild.discovery_splash_url` to get the discovery splash image asset. (:issue:`2482`)" msgstr "ディスカバリースプラッシュイメージアセットを取得するために :attr:`Guild.discovery_splash_url` を追加しました。(:issue:`2482`)" -#: ../../whats_new.rst:804 +#: ../../whats_new.rst:837 msgid "Add :attr:`Guild.rules_channel` to get the rules channel of public guilds. (:issue:`2482`)" msgstr "パブリック・ギルドのルール・チャンネルを取得するために :attr:`Guild.rules_channel` を追加しました。(:issue:`2482`)" -#: ../../whats_new.rst:804 +#: ../../whats_new.rst:837 msgid "It should be noted that this feature is restricted to those who are either in Server Discovery or planning to be there." msgstr "なお、この機能はサーバーディスカバリーに参加されている方、または参加予定の方に限定しています。" -#: ../../whats_new.rst:806 +#: ../../whats_new.rst:839 msgid "Add support for message flags via :attr:`Message.flags` and :class:`MessageFlags`. (:issue:`2433`)" msgstr ":attr:`Message.flags` と :class:`MessageFlags` によるメッセージフラグのサポートを追加しました。(:issue:`2433`)" -#: ../../whats_new.rst:807 +#: ../../whats_new.rst:840 msgid "Add :attr:`User.system` and :attr:`Profile.system` to know whether a user is an official Discord Trust and Safety account." msgstr "ユーザーがDiscord Trust and Safetyの公式アカウントであるかどうかを知るために、 :attr:`User.system` と :attr:`Profile.system` を追加しました。" -#: ../../whats_new.rst:808 +#: ../../whats_new.rst:841 msgid "Add :attr:`Profile.team_user` to check whether a user is a member of a team." msgstr "ユーザーがチームのメンバーであるかどうかを確認するために :attr:`Profile.team_user` を追加しました。" -#: ../../whats_new.rst:809 +#: ../../whats_new.rst:842 msgid "Add :meth:`Attachment.to_file` to easily convert attachments to :class:`File` for sending." msgstr "添付ファイルを簡単に :class:`File` に変換して送信できるように :meth:`Attachment.to_file` を追加。" -#: ../../whats_new.rst:813 +#: ../../whats_new.rst:846 msgid "Add certain aliases to :class:`Permissions` to match the UI better. (:issue:`2496`)" msgstr "UIにマッチするように、特定のエイリアスを :class:`Permissions` に追加しました。(:issue:`2496`)" -#: ../../whats_new.rst:811 +#: ../../whats_new.rst:844 msgid ":attr:`Permissions.manage_permissions`" msgstr ":attr:`Permissions.manage_permissions`" -#: ../../whats_new.rst:812 +#: ../../whats_new.rst:845 msgid ":attr:`Permissions.view_channel`" msgstr ":attr:`Permissions.view_channel`" -#: ../../whats_new.rst:813 +#: ../../whats_new.rst:846 msgid ":attr:`Permissions.use_external_emojis`" msgstr ":attr:`Permissions.use_external_emojis`" -#: ../../whats_new.rst:815 +#: ../../whats_new.rst:848 msgid "Add support for passing keyword arguments when creating :class:`Permissions`." msgstr ":class:`Permissions` を作成する際に、キーワード引数を渡せるようになりました。" -#: ../../whats_new.rst:817 +#: ../../whats_new.rst:850 msgid "Add support for custom activities via :class:`CustomActivity`. (:issue:`2400`)" msgstr ":class:`CustomActivity` によるカスタムアクティビティーのサポートを追加しました。(:issue:`2400`)" -#: ../../whats_new.rst:817 +#: ../../whats_new.rst:850 msgid "Note that as of now, bots cannot send custom activities yet." msgstr "なお、現在のところ、ボットはまだカスタムアクティビティを送信できません。" -#: ../../whats_new.rst:819 +#: ../../whats_new.rst:852 msgid "Add support for :func:`on_invite_create` and :func:`on_invite_delete` events." msgstr ":func:`on_invite_create` と :func:`on_invite_delete` イベントのサポートを追加しました。" -#: ../../whats_new.rst:822 +#: ../../whats_new.rst:855 msgid "Add support for clearing a specific reaction emoji from a message." msgstr "メッセージから特定のリアクション絵文字を消去する機能を追加しました。" -#: ../../whats_new.rst:821 +#: ../../whats_new.rst:854 msgid ":meth:`Message.clear_reaction` and :meth:`Reaction.clear` methods." msgstr ":meth:`Message.clear_reaction` および :meth:`Reaction.clear` メソッドを使用します。" -#: ../../whats_new.rst:822 +#: ../../whats_new.rst:855 msgid ":func:`on_raw_reaction_clear_emoji` and :func:`on_reaction_clear_emoji` events." msgstr ":func:`on_raw_reaction_clear_emoji` と :func:`on_reaction_clear_emoji` イベントです。" -#: ../../whats_new.rst:824 +#: ../../whats_new.rst:857 msgid "Add :func:`utils.sleep_until` helper to sleep until a specific datetime. (:issue:`2517`, :issue:`2519`)" msgstr "特定の日付までスリープさせる :func:`utils.sleep_until` ヘルパーを追加しました。(:issue:`2517`、:issue:`2519`)" -#: ../../whats_new.rst:825 +#: ../../whats_new.rst:858 msgid "|commands| Add support for teams and :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` to have multiple bot owners. (:issue:`2239`)" msgstr "|commands| チームと :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` が複数のボットオーナーを持つためのサポートを追加しました。(:issue:`2239`)" -#: ../../whats_new.rst:826 +#: ../../whats_new.rst:859 msgid "|commands| Add new :attr:`BucketType.role <.ext.commands.BucketType.role>` bucket type. (:issue:`2201`)" msgstr "|commands| 新しい :attr:`BucketType.role <.ext.commands.BucketType.role>` のバケットタイプを追加しました。(:issue:`2201`)です。" -#: ../../whats_new.rst:827 +#: ../../whats_new.rst:860 msgid "|commands| Expose :attr:`Command.cog <.ext.commands.Command.cog>` property publicly. (:issue:`2360`)" msgstr "|commands| :attr:`Command.cog <.ext.commands.Command.cog>` のプロパティを公開します。(:issue:`2360`)" -#: ../../whats_new.rst:828 +#: ../../whats_new.rst:861 msgid "|commands| Add non-decorator interface for adding checks to commands via :meth:`Command.add_check <.ext.commands.Command.add_check>` and :meth:`Command.remove_check <.ext.commands.Command.remove_check>`. (:issue:`2411`)" msgstr "|commands| :meth:`Command.add_check <.ext.commands.Command.add_check>` および :meth:`Command.remove_check <.ext.commands.Command.remove_check>` によりコマンドにチェックを追加する非デコレーターインターフェイスを追加しました。(:issue:`2411`)" -#: ../../whats_new.rst:829 +#: ../../whats_new.rst:862 msgid "|commands| Add :func:`has_guild_permissions <.ext.commands.has_guild_permissions>` check. (:issue:`2460`)" msgstr "|commands| :func:`has_guild_permissions <.ext.commands.has_guild_permissions>` のチェックを追加しました。(:issue:`2460`)" -#: ../../whats_new.rst:830 +#: ../../whats_new.rst:863 msgid "|commands| Add :func:`bot_has_guild_permissions <.ext.commands.bot_has_guild_permissions>` check. (:issue:`2460`)" msgstr "|commands| :func:`has_guild_permissions <.ext.commands.bot_has_guild_permissions>` のチェックを追加しました。(:issue:`2460`)" -#: ../../whats_new.rst:831 +#: ../../whats_new.rst:864 msgid "|commands| Add ``predicate`` attribute to checks decorated with :func:`~.ext.commands.check`." msgstr "|commands| :func:`~.ext.commands.check` で装飾されたチェックに ``predicate`` 属性を追加しました。" -#: ../../whats_new.rst:832 +#: ../../whats_new.rst:865 msgid "|commands| Add :func:`~.ext.commands.check_any` check to logical OR multiple checks." msgstr "|commands| :func:`~.ext.commands.check_any` チェックを論理的 OR 複数のチェックに追加しました。" -#: ../../whats_new.rst:833 +#: ../../whats_new.rst:866 msgid "|commands| Add :func:`~.ext.commands.max_concurrency` to allow only a certain amount of users to use a command concurrently before waiting or erroring." msgstr "|commands| 待ち時間やエラーになる前に、ある一定のユーザーだけがコマンドを同時に使用できるようにするための :func:`~.ext.commands.max_concurrency` を追加しました。" -#: ../../whats_new.rst:834 +#: ../../whats_new.rst:867 msgid "|commands| Add support for calling a :class:`~.ext.commands.Command` as a regular function." msgstr "|commands| :class:`~.ext.commands.Command` を通常の関数として呼び出すためのサポートを追加しました。" -#: ../../whats_new.rst:835 +#: ../../whats_new.rst:868 msgid "|tasks| :meth:`Loop.add_exception_type <.ext.tasks.Loop.add_exception_type>` now allows multiple exceptions to be set. (:issue:`2333`)" msgstr "|tasks| :meth:`Loop.add_exception_type <.ext.tasks.Loop.add_exception_type>` が、複数の例外を設定できるようになりました。(:issue:`2333`)" -#: ../../whats_new.rst:836 +#: ../../whats_new.rst:869 msgid "|tasks| Add :attr:`Loop.next_iteration <.ext.tasks.Loop.next_iteration>` property. (:issue:`2305`)" msgstr "|tasks| Add :attr:`Loop.next_iteration <.ext.tasks.Loop.next_iteration>` プロパティを追加しました。(:issue:`2305`)" -#: ../../whats_new.rst:841 +#: ../../whats_new.rst:874 msgid "Fix issue with permission resolution sometimes failing for guilds with no owner." msgstr "所有者がいないギルドで権限解決に失敗することがある問題を修正しました。" -#: ../../whats_new.rst:842 +#: ../../whats_new.rst:875 msgid "Tokens are now stripped upon use. (:issue:`2135`)" msgstr "トークンは、使用時に剥奪されるようになりました。(:issue:`2135`)" -#: ../../whats_new.rst:843 +#: ../../whats_new.rst:876 msgid "Passing in a ``name`` is no longer required for :meth:`Emoji.edit`. (:issue:`2368`)" msgstr ":meth:`Emoji.edit` に ``name`` を渡す必要はなくなりました。(:issue:`2368`)" -#: ../../whats_new.rst:844 +#: ../../whats_new.rst:877 msgid "Fix issue with webhooks not re-raising after retries have run out. (:issue:`2272`, :issue:`2380`)" msgstr "Webhooks がリトライを使い切った後に再レイズしない問題を修正しました。(:issue:`2272`, :issue:`2380`)" -#: ../../whats_new.rst:845 +#: ../../whats_new.rst:878 msgid "Fix mismatch in URL handling in :func:`utils.escape_markdown`. (:issue:`2420`)" msgstr ":func:`utils.escape_markdown` のURLハンドリングにおけるミスマッチを修正しました。(:issue:`2420`)" -#: ../../whats_new.rst:846 +#: ../../whats_new.rst:879 msgid "Fix issue with ports being read in little endian when they should be big endian in voice connections. (:issue:`2470`)" msgstr "音声接続において、ビッグエンディアンであるべきポートがリトルエンディアンで読み込まれる問題を修正しました。(:issue:`2470`)" -#: ../../whats_new.rst:847 +#: ../../whats_new.rst:880 msgid "Fix :meth:`Member.mentioned_in` not taking into consideration the message's guild." msgstr "メッセージのギルドが考慮されない :meth:`Member.mentioned_in` を修正しました。" -#: ../../whats_new.rst:848 +#: ../../whats_new.rst:881 msgid "Fix bug with moving channels when there are gaps in positions due to channel deletion and creation." msgstr "チャンネルの削除と作成によりポジションにギャップがある場合、チャンネルを移動する不具合を修正。" -#: ../../whats_new.rst:849 +#: ../../whats_new.rst:882 msgid "Fix :func:`on_shard_ready` not triggering when ``fetch_offline_members`` is disabled. (:issue:`2504`)" msgstr "``fetch_offline_members`` が無効の場合、 :func:`on_shard_ready` が発火されない問題を修正しました。(:issue:`2504`)" -#: ../../whats_new.rst:850 +#: ../../whats_new.rst:883 msgid "Fix issue with large sharded bots taking too long to actually dispatch :func:`on_ready`." msgstr "シャードを使用している大きなBotが :func:`on_ready` を実際に発火するのに長い時間を掛けていた問題を修正しました。" -#: ../../whats_new.rst:851 +#: ../../whats_new.rst:884 msgid "Fix issue with fetching group DM based invites in :meth:`Client.fetch_invite`." msgstr ":meth:`Client.fetch_invite` でグループDMベースの招待を取得する際の問題を修正しました。" -#: ../../whats_new.rst:852 +#: ../../whats_new.rst:885 msgid "Fix out of order files being sent in webhooks when there are 10 files." msgstr "10つのファイルをWebhookで送信する際、ファイルの順序が狂う問題を修正しました。" -#: ../../whats_new.rst:853 +#: ../../whats_new.rst:886 msgid "|commands| Extensions that fail internally due to ImportError will no longer raise :exc:`~.ext.commands.ExtensionNotFound`. (:issue:`2244`, :issue:`2275`, :issue:`2291`)" msgstr "|commands| ImportErrorによって内部的に失敗する拡張機能は、 :exc:`~.ext.commands.ExtensionNotFound` を発生させなくなりました。(:issue:`2244`, :issue:`2275`, :issue:`2291`)" -#: ../../whats_new.rst:854 +#: ../../whats_new.rst:887 msgid "|commands| Updating the :attr:`Paginator.suffix <.ext.commands.Paginator.suffix>` will not cause out of date calculations. (:issue:`2251`)" msgstr "|commands| :attr:`Paginator.suffix <.ext.commands.Paginator.suffix>` を更新しても、計算が古くならないようにしました。(:issue:`2251`)" -#: ../../whats_new.rst:855 +#: ../../whats_new.rst:888 msgid "|commands| Allow converters from custom extension packages. (:issue:`2369`, :issue:`2374`)" msgstr "|commands| カスタム拡張パッケージからのコンバータを許可します。(:issue:`2369`, :issue:`2374`) のようになります。" -#: ../../whats_new.rst:856 +#: ../../whats_new.rst:889 msgid "|commands| Fix issue with paginator prefix being ``None`` causing empty pages. (:issue:`2471`)" msgstr "|commands| paginator のプレフィックスが ``None`` であるために空のページが発生する問題を修正しました。(:issue:`2471`)" -#: ../../whats_new.rst:857 +#: ../../whats_new.rst:890 msgid "|commands| :class:`~.commands.Greedy` now ignores parsing errors rather than propagating them." msgstr "|commands| :class:`~.commands.Greedy` はパージングエラーを伝播するのではなく、無視するようになりました。" -#: ../../whats_new.rst:858 +#: ../../whats_new.rst:891 msgid "|commands| :meth:`Command.can_run <.ext.commands.Command.can_run>` now checks whether a command is disabled." msgstr "|commands| :meth:`Command.can_run <.ext.commands.Command.can_run>` がコマンドが無効かどうかをチェックするようになりました。" -#: ../../whats_new.rst:859 +#: ../../whats_new.rst:892 msgid "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` now takes into consideration nickname mentions. (:issue:`2489`)" msgstr "|commands| :attr:`HelpCommand.clean_prefix <.ext.commands.HelpCommand.clean_prefix>` がニックネームのメンションを考慮するようになりました。 (:issue:`2489`)" -#: ../../whats_new.rst:860 +#: ../../whats_new.rst:893 msgid "|commands| :meth:`Context.send_help <.ext.commands.Context.send_help>` now properly propagates to the :meth:`HelpCommand.on_help_command_error <.ext.commands.HelpCommand.on_help_command_error>` handler." msgstr "|commands| :meth:`Context.send_help <.ext.commands.Context.send_help>` が :meth:`HelpCommand.on_help_command_error <.ext.commands.HelpCommand.on_help_command_error>` ハンドラに正しく伝播するようになりました。" -#: ../../whats_new.rst:865 +#: ../../whats_new.rst:898 msgid "The library now fully supports Python 3.8 without warnings." msgstr "ライブラリは警告なしに Python 3.8 を完全にサポートするようになりました。" -#: ../../whats_new.rst:866 +#: ../../whats_new.rst:899 msgid "Bump the dependency of ``websockets`` to 8.0 for those who can use it. (:issue:`2453`)" msgstr "依存ライブラリ ``websockets`` のバージョンを 8.0 に上げました。(:issue:`2453`)" -#: ../../whats_new.rst:867 +#: ../../whats_new.rst:900 msgid "Due to Discord providing :class:`Member` data in mentions, users will now be upgraded to :class:`Member` more often if mentioned." msgstr "Discordがメンションで :class:`Member` データを提供するようになったため、メンションされたユーザーが :class:`Member` により多くの機会でアップグレードされるようになりました。" -#: ../../whats_new.rst:868 +#: ../../whats_new.rst:901 msgid ":func:`utils.escape_markdown` now properly escapes new quote markdown." msgstr ":func:`utils.escape_markdown` が新しい引用マークダウンを正しくエスケープするようになりました。" -#: ../../whats_new.rst:869 +#: ../../whats_new.rst:902 msgid "The message cache can now be disabled by passing ``None`` to ``max_messages`` in :class:`Client`." msgstr "メッセージキャッシュを :class:`Client` の ``max_messages`` に ``None`` を渡すことで無効にできるようになりました。" -#: ../../whats_new.rst:870 +#: ../../whats_new.rst:903 msgid "The default message cache size has changed from 5000 to 1000 to accommodate small bots." msgstr "デフォルトのメッセージキャッシュサイズは、小さなボットに対応するために5000から1000に変更されました。" -#: ../../whats_new.rst:871 +#: ../../whats_new.rst:904 msgid "Lower memory usage by only creating certain objects as needed in :class:`Role`." msgstr ":class:`Role` にて、必要な場合のみ特定のオブジェクトを作成することによりメモリ使用量を削減しました。" -#: ../../whats_new.rst:872 +#: ../../whats_new.rst:905 msgid "There is now a sleep of 5 seconds before re-IDENTIFYing during a reconnect to prevent long loops of session invalidation." msgstr "セッションの無効化の長いループを防ぐために、再接続中に再度IDENTIFYする前に5秒間待つようになりました。" -#: ../../whats_new.rst:874 +#: ../../whats_new.rst:907 msgid "The rate limiting code now uses millisecond precision to have more granular rate limit handling." msgstr "レート制限コードは、より細かいレート制限処理を行うためにミリ秒の精度を使用するようになりました。" -#: ../../whats_new.rst:874 +#: ../../whats_new.rst:907 msgid "Along with that, the rate limiting code now uses Discord's response to wait. If you need to use the system clock again for whatever reason, consider passing ``assume_synced_clock`` in :class:`Client`." msgstr "それに伴い、レート制限コードはDiscordのレスポンスを使用して待つようになりました。 何らかの理由でシステムクロックを使用する必要がある場合は、 :class:`Client` で ``assume_synced_clock`` を渡すことを検討してください。" -#: ../../whats_new.rst:876 +#: ../../whats_new.rst:909 msgid "The performance of :attr:`Guild.default_role` has been improved from O(N) to O(1). (:issue:`2375`)" msgstr ":attr:`Guild.default_role` のパフォーマンスが O(N) から O(1) に改善されました。 (:issue:`2375`)" -#: ../../whats_new.rst:877 +#: ../../whats_new.rst:910 msgid "The performance of :attr:`Member.roles` has improved due to usage of caching to avoid surprising performance traps." msgstr "予期しないパフォーマンストラップを避けるために、キャッシュを使用して :attr:`Member.roles` のパフォーマンスを改善しました。" -#: ../../whats_new.rst:878 +#: ../../whats_new.rst:911 msgid "The GC is manually triggered during things that cause large deallocations (such as guild removal) to prevent memory fragmentation." msgstr "メモリの断片化を防ぐため、大規模なメモリの割り当て解除 (ギルドの除去など) が引き起こされた後に手動でガベージコレクションを行うようになりました。" -#: ../../whats_new.rst:879 +#: ../../whats_new.rst:912 msgid "There have been many changes to the documentation for fixes both for usability, correctness, and to fix some linter errors. Thanks to everyone who contributed to those." msgstr "ユーザビリティや正確性を向上させ、リンターエラーを修正するため、ドキュメントに多くの変更がありました。 貢献したすべての人に感謝します。" -#: ../../whats_new.rst:880 +#: ../../whats_new.rst:913 msgid "The loading of the opus module has been delayed which would make the result of :func:`opus.is_loaded` somewhat surprising." msgstr "opus モジュールの読み込みを遅延させるようにしました。このため :func:`opus.is_loaded` の結果が予想しないものになるかもしれません。" -#: ../../whats_new.rst:881 +#: ../../whats_new.rst:914 msgid "|commands| Usernames prefixed with @ inside DMs will properly convert using the :class:`User` converter. (:issue:`2498`)" msgstr "|commands| DM内の@で始まるユーザー名が、 :class:`User` コンバータを使用したとき正しく変換されるようになりました。 (:issue:`2498`)" -#: ../../whats_new.rst:882 +#: ../../whats_new.rst:915 msgid "|tasks| The task sleeping time will now take into consideration the amount of time the task body has taken before sleeping. (:issue:`2516`)" msgstr "|tasks| タスクの待ち時間が、タスク本体が実行するのにかかった時間を考慮に入れるようになりました。 (:issue:`2516`)" -#: ../../whats_new.rst:887 +#: ../../whats_new.rst:920 msgid "v1.2.5" msgstr "v1.2.5" -#: ../../whats_new.rst:892 +#: ../../whats_new.rst:925 msgid "Fix a bug that caused crashes due to missing ``animated`` field in Emoji structures in reactions." msgstr "絵文字構造の ``animated`` フィールドが存在しないとしてクラッシュするバグを修正しました。" -#: ../../whats_new.rst:897 +#: ../../whats_new.rst:930 msgid "v1.2.4" msgstr "v1.2.4" -#: ../../whats_new.rst:902 +#: ../../whats_new.rst:935 msgid "Fix a regression when :attr:`Message.channel` would be ``None``." msgstr ":attr:`Message.channel` が ``None`` になるリグレッションを修正しました。" -#: ../../whats_new.rst:903 +#: ../../whats_new.rst:936 msgid "Fix a regression where :attr:`Message.edited_at` would not update during edits." msgstr ":attr:`Message.edited_at` が編集中に更新されないリグレッションを修正しました。" -#: ../../whats_new.rst:904 +#: ../../whats_new.rst:937 msgid "Fix a crash that would trigger during message updates (:issue:`2265`, :issue:`2287`)." msgstr "メッセージの更新中に引き起こされるクラッシュを修正しました。(:issue:`2265`, :issue:`2287`)" -#: ../../whats_new.rst:905 +#: ../../whats_new.rst:938 msgid "Fix a bug when :meth:`VoiceChannel.connect` would not return (:issue:`2274`, :issue:`2372`, :issue:`2373`, :issue:`2377`)." msgstr ":meth:`VoiceChannel.connect` が応答しないバグを修正しました。(:issue:`2274`、 :issue:`2372`、 :issue:`2373`、 :issue:`2377`)" -#: ../../whats_new.rst:906 +#: ../../whats_new.rst:939 msgid "Fix a crash relating to token-less webhooks (:issue:`2364`)." msgstr "トークンのないWebhookに関するクラッシュを修正しました。(:issue:`2364`)" -#: ../../whats_new.rst:907 +#: ../../whats_new.rst:940 msgid "Fix issue where :attr:`Guild.premium_subscription_count` would be ``None`` due to a Discord bug. (:issue:`2331`, :issue:`2376`)." msgstr "Discord バグにより :attr:`Guild.premium_subscription_count` が ``None`` になる問題を修正しました。(:issue:`2331`, :issue:`2376`)" -#: ../../whats_new.rst:912 +#: ../../whats_new.rst:945 msgid "v1.2.3" msgstr "v1.2.3" -#: ../../whats_new.rst:917 +#: ../../whats_new.rst:950 msgid "Fix an AttributeError when accessing :attr:`Member.premium_since` in :func:`on_member_update`. (:issue:`2213`)" msgstr ":func:`on_member_update` で :attr:`Member.premium_since` にアクセスした際の AttributeError を修正しました。 (:issue:`2213`)" -#: ../../whats_new.rst:918 +#: ../../whats_new.rst:951 msgid "Handle :exc:`asyncio.CancelledError` in :meth:`abc.Messageable.typing` context manager. (:issue:`2218`)" msgstr ":meth:`abc.Messageable.typing` コンテキストマネージャでの :exc:`asyncio.CanceledError` を処理するようにしました。 (:issue:`2218`)" -#: ../../whats_new.rst:919 +#: ../../whats_new.rst:952 msgid "Raise the max encoder bitrate to 512kbps to account for nitro boosting. (:issue:`2232`)" msgstr "ニトロブーストを考慮し、最大エンコーダビットレートを512kbpsに引き上げ。 (:issue:`2232`)" -#: ../../whats_new.rst:920 +#: ../../whats_new.rst:953 msgid "Properly propagate exceptions in :meth:`Client.run`. (:issue:`2237`)" msgstr ":meth:`Client.run` にて例外を適切に伝播するようにしました。(:issue:`2237`)" -#: ../../whats_new.rst:921 +#: ../../whats_new.rst:954 msgid "|commands| Ensure cooldowns are properly copied when used in cog level ``command_attrs``." msgstr "|commands| コグレベル ``command_attrs`` で使用されるクールダウンが正しくコピーされるようにしました。" -#: ../../whats_new.rst:926 +#: ../../whats_new.rst:959 msgid "v1.2.2" msgstr "v1.2.2" -#: ../../whats_new.rst:931 +#: ../../whats_new.rst:964 msgid "Audit log related attribute access have been fixed to not error out when they shouldn't have." msgstr "監査ログ関連の属性アクセスは、本来すべきでないときにエラーを起こさないよう修正されました。" -#: ../../whats_new.rst:936 +#: ../../whats_new.rst:969 msgid "v1.2.1" msgstr "v1.2.1" -#: ../../whats_new.rst:941 +#: ../../whats_new.rst:974 msgid ":attr:`User.avatar_url` and related attributes no longer raise an error." msgstr ":attr:`User.avatar_url` と関連する属性がエラーを引き起こさないように修正しました。" -#: ../../whats_new.rst:942 +#: ../../whats_new.rst:975 msgid "More compatibility shims with the ``enum.Enum`` code." msgstr "``enum.Enum`` コードの互換性が向上しました。" -#: ../../whats_new.rst:947 +#: ../../whats_new.rst:980 msgid "v1.2.0" msgstr "v1.2.0" -#: ../../whats_new.rst:949 +#: ../../whats_new.rst:982 msgid "This update mainly brings performance improvements and various nitro boosting attributes (referred to in the API as \"premium guilds\")." msgstr "今回のアップデートでは、主にパフォーマンスの向上と、さまざまなニトロブースト属性(APIでは「プレミアムギルド」と呼ばれます) が追加されました。" -#: ../../whats_new.rst:954 +#: ../../whats_new.rst:987 msgid "Add :attr:`Guild.premium_tier` to query the guild's current nitro boost level." msgstr ":attr:`Guild.premium_tier` で、ギルドの現在のニトロブーストレベルが取得できます。" -#: ../../whats_new.rst:955 +#: ../../whats_new.rst:988 msgid "Add :attr:`Guild.emoji_limit`, :attr:`Guild.bitrate_limit`, :attr:`Guild.filesize_limit` to query the new limits of a guild when taking into consideration boosting." msgstr "ブーストを考慮してギルドの新しい制限を取得する :attr:`Guild.emoji_limit` 、 :attr:`Guild.bitrate_limit` 、 :attr:`Guild.filesize_limit` を追加しました。" -#: ../../whats_new.rst:956 +#: ../../whats_new.rst:989 msgid "Add :attr:`Guild.premium_subscription_count` to query how many members are boosting a guild." msgstr "ギルドをブーストしているメンバー数を取得する :attr:`Guild.premium_subscription_count` を追加しました。" -#: ../../whats_new.rst:957 +#: ../../whats_new.rst:990 msgid "Add :attr:`Member.premium_since` to query since when a member has boosted a guild." msgstr "メンバーがギルドをブーストし始めた日時を取得する :attr:`Member.premium_since` を追加しました。" -#: ../../whats_new.rst:958 +#: ../../whats_new.rst:991 msgid "Add :attr:`Guild.premium_subscribers` to query all the members currently boosting the guild." msgstr "現在ギルドをブーストしているメンバーをすべて取得する :attr:`Guild.premium_subscribers` を追加しました。" -#: ../../whats_new.rst:959 +#: ../../whats_new.rst:992 msgid "Add :attr:`Guild.system_channel_flags` to query the settings for a guild's :attr:`Guild.system_channel`." msgstr "ギルドの :attr:`Guild.system_channel` の設定を取得する :attr:`Guild.system_channel_flags` を追加しました。" -#: ../../whats_new.rst:960 +#: ../../whats_new.rst:993 msgid "This includes a new type named :class:`SystemChannelFlags`" msgstr ":class:`SystemChannelFlags` という新しい型も含まれます。" -#: ../../whats_new.rst:961 +#: ../../whats_new.rst:994 msgid "Add :attr:`Emoji.available` to query if an emoji can be used (within the guild or otherwise)." msgstr "絵文字が(ギルド内またはそれ以外で)利用できるかを確認する :attr:`Emoji.available` を追加しました。" -#: ../../whats_new.rst:962 +#: ../../whats_new.rst:995 msgid "Add support for animated icons in :meth:`Guild.icon_url_as` and :attr:`Guild.icon_url`." msgstr ":meth:`Guild.icon_url_as` と :attr:`Guild.icon_url` にアニメーションアイコンのサポートを追加しました。" -#: ../../whats_new.rst:963 +#: ../../whats_new.rst:996 msgid "Add :meth:`Guild.is_icon_animated`." msgstr ":meth:`Guild.is_icon_animated` を追加しました。" -#: ../../whats_new.rst:964 +#: ../../whats_new.rst:997 msgid "Add support for the various new :class:`MessageType` involving nitro boosting." msgstr "ニトロブーストに関する様々な新しい :class:`MessageType` のサポートを追加しました。" -#: ../../whats_new.rst:965 +#: ../../whats_new.rst:998 msgid "Add :attr:`VoiceRegion.india`. (:issue:`2145`)" msgstr ":attr:`VoiceRegion.india` を追加しました。 (:issue:`2145`)" -#: ../../whats_new.rst:966 +#: ../../whats_new.rst:999 msgid "Add :meth:`Embed.insert_field_at`. (:issue:`2178`)" msgstr ":meth:`Embed.insert_field_at` を追加しました。 (:issue:`2178`)" -#: ../../whats_new.rst:967 +#: ../../whats_new.rst:1000 msgid "Add a ``type`` attribute for all channels to their appropriate :class:`ChannelType`. (:issue:`2185`)" msgstr "すべてのチャンネルに対し、適切な :class:`ChannelType` を返す ``type`` 属性を追加しました。 (:issue:`2185` )" -#: ../../whats_new.rst:968 +#: ../../whats_new.rst:1001 msgid "Add :meth:`Client.fetch_channel` to fetch a channel by ID via HTTP. (:issue:`2169`)" msgstr "HTTP経由でチャンネルをIDにより取得する、 :meth:`Client.fetch_channel` を追加しました。(:issue:`2169`)" -#: ../../whats_new.rst:969 +#: ../../whats_new.rst:1002 msgid "Add :meth:`Guild.fetch_channels` to fetch all channels via HTTP. (:issue:`2169`)" msgstr "HTTP経由でチャンネルをすべて取得する、 :meth:`Guild.fetch_channels` を追加しました。(:issue:`2169`)" -#: ../../whats_new.rst:970 +#: ../../whats_new.rst:1003 msgid "|tasks| Add :meth:`Loop.stop <.ext.tasks.Loop.stop>` to gracefully stop a task rather than cancelling." msgstr "|tasks| タスクをキャンセルするのではなく、現在のタスクが終了後に停止させる :meth:`Loop.stop <.ext.tasks.Loop.stop>` を追加しました。" -#: ../../whats_new.rst:971 +#: ../../whats_new.rst:1004 msgid "|tasks| Add :meth:`Loop.failed <.ext.tasks.Loop.failed>` to query if a task had failed somehow." msgstr "|tasks| タスクが何らかの理由で失敗したかを調べる :meth:`Loop.failed <.ext.tasks.Loop.failed>` を追加しました。" -#: ../../whats_new.rst:972 +#: ../../whats_new.rst:1005 msgid "|tasks| Add :meth:`Loop.change_interval <.ext.tasks.Loop.change_interval>` to change the sleep interval at runtime (:issue:`2158`, :issue:`2162`)" msgstr "|tasks| 実行時に待機時間を変更できる :meth:`Loop.change_interval <.ext.tasks.Loop.change_interval>` を追加しました。(:issue:`2158`, :issue:`2162`)" -#: ../../whats_new.rst:977 +#: ../../whats_new.rst:1010 msgid "Fix internal error when using :meth:`Guild.prune_members`." msgstr ":meth:`Guild.prune_members` を使用した場合の内部エラーを修正しました。" -#: ../../whats_new.rst:978 +#: ../../whats_new.rst:1011 msgid "|commands| Fix :attr:`.Command.invoked_subcommand` being invalid in many cases." msgstr "|commands| 多くの場合において :attr:`.Command.invoked_subcommand` が誤っているのを修正しました。" -#: ../../whats_new.rst:979 +#: ../../whats_new.rst:1012 msgid "|tasks| Reset iteration count when the loop terminates and is restarted." msgstr "|tasks| ループが終了し、再起動されたときに反復回数をリセットするようにしました。" -#: ../../whats_new.rst:980 +#: ../../whats_new.rst:1013 msgid "|tasks| The decorator interface now works as expected when stacking (:issue:`2154`)" msgstr "|tasks| デコレータインターフェースをスタックした時に期待通り動作するようになりました。 (:issue:`2154`)" -#: ../../whats_new.rst:986 +#: ../../whats_new.rst:1019 msgid "Improve performance of all Enum related code significantly." msgstr "列挙型に関連するすべてのコードのパフォーマンスを大幅に向上させました。" -#: ../../whats_new.rst:986 +#: ../../whats_new.rst:1019 msgid "This was done by replacing the ``enum.Enum`` code with an API compatible one." msgstr "これは、 ``enum.Enum`` コードを API 互換のコードに置き換えることによって行われました。" -#: ../../whats_new.rst:987 +#: ../../whats_new.rst:1020 msgid "This should not be a breaking change for most users due to duck-typing." msgstr "ダックタイピングを使用しているため、ほとんどのユーザーにとっては破壊的変更ではありません。" -#: ../../whats_new.rst:988 +#: ../../whats_new.rst:1021 msgid "Improve performance of message creation by about 1.5x." msgstr "メッセージ作成のパフォーマンスを約1.5倍向上させました。" -#: ../../whats_new.rst:989 +#: ../../whats_new.rst:1022 msgid "Improve performance of message editing by about 1.5-4x depending on payload size." msgstr "メッセージ編集のパフォーマンスが約1.5~4倍向上しました。(内容のサイズに依存します)" -#: ../../whats_new.rst:990 +#: ../../whats_new.rst:1023 msgid "Improve performance of attribute access on :class:`Member` about by 2x." msgstr ":class:`Member` の属性へのアクセスのパフォーマンスが2倍向上しました。" -#: ../../whats_new.rst:991 +#: ../../whats_new.rst:1024 msgid "Improve performance of :func:`utils.get` by around 4-6x depending on usage." msgstr ":func:`utils.get` のパフォーマンスを、使用状況に応じて約 4-6倍 向上させました。" -#: ../../whats_new.rst:992 +#: ../../whats_new.rst:1025 msgid "Improve performance of event parsing lookup by around 2.5x." msgstr "イベント解析中のルックアップのパフォーマンスを約2.5倍向上させました。" -#: ../../whats_new.rst:993 +#: ../../whats_new.rst:1026 msgid "Keyword arguments in :meth:`Client.start` and :meth:`Client.run` are now validated (:issue:`953`, :issue:`2170`)" msgstr ":meth:`Client.start` と :meth:`Client.run` のキーワード引数を検証するようにしました。 (:issue:`953`, :issue:`2170`)" -#: ../../whats_new.rst:994 +#: ../../whats_new.rst:1027 msgid "The Discord error code is now shown in the exception message for :exc:`HTTPException`." msgstr ":exc:`HTTPException` の例外メッセージにDiscordのエラーコードが表示されるようになりました。" -#: ../../whats_new.rst:995 +#: ../../whats_new.rst:1028 msgid "Internal tasks launched by the library will now have their own custom ``__repr__``." msgstr "ライブラリによって実行された内部タスクに独自のカスタム ``__repr__`` を追加しました。" -#: ../../whats_new.rst:996 +#: ../../whats_new.rst:1029 msgid "All public facing types should now have a proper and more detailed ``__repr__``." msgstr "すべての公開された型に、適切でより詳細な ``__repr__`` を追加しました。" -#: ../../whats_new.rst:997 +#: ../../whats_new.rst:1030 msgid "|tasks| Errors are now logged via the standard :mod:`py:logging` module." msgstr "|tasks| 標準の :mod:`py:logging` モジュールを介してエラーが記録されるようになりました。" -#: ../../whats_new.rst:1002 +#: ../../whats_new.rst:1035 msgid "v1.1.1" msgstr "v1.1.1" -#: ../../whats_new.rst:1007 +#: ../../whats_new.rst:1040 msgid "Webhooks do not overwrite data on retrying their HTTP requests (:issue:`2140`)" msgstr "WebhookがHTTPリクエストを再試行する時にデータを上書きしないようにしました。 (:issue:`2140`)" -#: ../../whats_new.rst:1012 +#: ../../whats_new.rst:1045 msgid "Add back signal handling to :meth:`Client.run` due to issues some users had with proper cleanup." msgstr "一部のユーザーが適切なクリーンアップを行うときに問題が生じていたため、 :meth:`Client.run` にシグナル処理を再度追加しました。" -#: ../../whats_new.rst:1017 +#: ../../whats_new.rst:1050 msgid "v1.1.0" msgstr "v1.1.0" -#: ../../whats_new.rst:1022 +#: ../../whats_new.rst:1055 msgid "**There is a new extension dedicated to making background tasks easier.**" msgstr "**バックグラウンドタスクを簡単にするための新しい拡張機能が追加されました。**" -#: ../../whats_new.rst:1023 +#: ../../whats_new.rst:1056 msgid "You can check the documentation here: :ref:`ext_tasks_api`." msgstr "使い方の説明は、 :ref:`ext_tasks_api` で確認できます。" -#: ../../whats_new.rst:1024 +#: ../../whats_new.rst:1057 msgid "Add :attr:`Permissions.stream` permission. (:issue:`2077`)" msgstr ":attr:`Permissions.stream` 権限を追加しました。 (:issue:`2077`)" -#: ../../whats_new.rst:1025 +#: ../../whats_new.rst:1058 msgid "Add equality comparison and hash support to :class:`Asset`" msgstr ":class:`Asset` に等価比較とハッシュサポートを追加しました。" -#: ../../whats_new.rst:1026 +#: ../../whats_new.rst:1059 msgid "Add ``compute_prune_members`` parameter to :meth:`Guild.prune_members` (:issue:`2085`)" msgstr ":meth:`Guild.prune_members` に ``compute_prune_members`` パラメータを追加しました。 (:issue:`2085`)" -#: ../../whats_new.rst:1027 +#: ../../whats_new.rst:1060 msgid "Add :attr:`Client.cached_messages` attribute to fetch the message cache (:issue:`2086`)" msgstr "メッセージキャッシュを取得する :attr:`Client.cached_messages` 属性を追加しました。 (:issue:`2086`)" -#: ../../whats_new.rst:1028 +#: ../../whats_new.rst:1061 msgid "Add :meth:`abc.GuildChannel.clone` to clone a guild channel. (:issue:`2093`)" msgstr "ギルドのチャンネルをコピーする :meth:`abc.GuildChannel.clone` メソッドが追加されました。( :issue:`2093` )" -#: ../../whats_new.rst:1029 +#: ../../whats_new.rst:1062 msgid "Add ``delay`` keyword-only argument to :meth:`Message.delete` (:issue:`2094`)" msgstr ":meth:`Message.delete` にキーワード限定引数 ``delay`` が追加されました。( :issue:`2094` )" -#: ../../whats_new.rst:1030 +#: ../../whats_new.rst:1063 msgid "Add support for ``<:name:id>`` when adding reactions (:issue:`2095`)" msgstr "``<:name:id>`` のフォーマットでリアクションを追加できるようになりました。( :issue:`2095` )" -#: ../../whats_new.rst:1031 +#: ../../whats_new.rst:1064 msgid "Add :meth:`Asset.read` to fetch the bytes content of an asset (:issue:`2107`)" msgstr "アセットを ``bytes`` オブジェクトとして取得する :meth:`Asset.read` メソッドが追加されました( :issue:`2107` )" -#: ../../whats_new.rst:1032 +#: ../../whats_new.rst:1065 msgid "Add :meth:`Attachment.read` to fetch the bytes content of an attachment (:issue:`2118`)" msgstr "添付ファイルを ``bytes`` オブジェクトとして取得する :meth:`Attachment.read` メソッドが追加されました( :issue:`2118` )" -#: ../../whats_new.rst:1033 +#: ../../whats_new.rst:1066 msgid "Add support for voice kicking by passing ``None`` to :meth:`Member.move_to`." msgstr ":meth:`Member.move_to` に ``None`` を渡すことでボイスチャンネルから強制切断できるようになりました。" -#: ../../whats_new.rst:1036 -#: ../../whats_new.rst:1057 -#: ../../whats_new.rst:1076 +#: ../../whats_new.rst:1069 +#: ../../whats_new.rst:1090 +#: ../../whats_new.rst:1109 msgid "``discord.ext.commands``" msgstr "``discord.ext.commands``" -#: ../../whats_new.rst:1038 +#: ../../whats_new.rst:1071 msgid "Add new :func:`~.commands.dm_only` check." msgstr ":func:`~.commands.dm_only` チェックが追加されました。" -#: ../../whats_new.rst:1039 +#: ../../whats_new.rst:1072 msgid "Support callable converters in :data:`~.commands.Greedy`" msgstr "呼び出し可能オブジェクトのコンバーターを :data:`~.commands.Greedy` で使えるようになりました。" -#: ../../whats_new.rst:1040 +#: ../../whats_new.rst:1073 msgid "Add new :class:`~.commands.MessageConverter`." msgstr ":class:`~.commands.MessageConverter` が追加されました。" -#: ../../whats_new.rst:1041 +#: ../../whats_new.rst:1074 msgid "This allows you to use :class:`Message` as a type hint in functions." msgstr "これにより、 :class:`Message` を関数の型ヒントで使えるようになりました。" -#: ../../whats_new.rst:1042 +#: ../../whats_new.rst:1075 msgid "Allow passing ``cls`` in the :func:`~.commands.group` decorator (:issue:`2061`)" msgstr ":func:`~.commands.group` に ``cls`` を渡せるようになりました( :issue:`2061` )" -#: ../../whats_new.rst:1043 +#: ../../whats_new.rst:1076 msgid "Add :attr:`.Command.parents` to fetch the parents of a command (:issue:`2104`)" msgstr "親コマンドを取得する :attr:`.Command.parents` が追加されました。( :issue:`2104` )" -#: ../../whats_new.rst:1049 +#: ../../whats_new.rst:1082 msgid "Fix :exc:`AttributeError` when using ``__repr__`` on :class:`Widget`." msgstr ":class:`Widget` の ``__repr__`` で :exc:`AttributeError` が発生するバグを修正しました。" -#: ../../whats_new.rst:1050 +#: ../../whats_new.rst:1083 msgid "Fix issue with :attr:`abc.GuildChannel.overwrites` returning ``None`` for keys." msgstr ":attr:`abc.GuildChannel.overwrites` のキーが ``None`` になるバグを修正しました。" -#: ../../whats_new.rst:1051 +#: ../../whats_new.rst:1084 msgid "Remove incorrect legacy NSFW checks in e.g. :meth:`TextChannel.is_nsfw`." msgstr ":meth:`TextChannel.is_nsfw` 等でのNSFWのチェックを修正しました。" -#: ../../whats_new.rst:1052 +#: ../../whats_new.rst:1085 msgid "Fix :exc:`UnboundLocalError` when :class:`RequestsWebhookAdapter` raises an error." msgstr ":class:`RequestsWebhookAdapter` でエラーが発生したときの :exc:`UnboundLocalError` を修正しました。" -#: ../../whats_new.rst:1053 +#: ../../whats_new.rst:1086 msgid "Fix bug where updating your own user did not update your member instances." msgstr "ボットのユーザーをアップデートしてもメンバーオブジェクトが更新されないバグを修正しました。" -#: ../../whats_new.rst:1054 +#: ../../whats_new.rst:1087 msgid "Tighten constraints of ``__eq__`` in :class:`Spotify` objects (:issue:`2113`, :issue:`2117`)" msgstr ":class:`Spotify` の ``__eq__`` の条件を厳しくしました。( :issue:`2113`, :issue:`2117` )" -#: ../../whats_new.rst:1059 +#: ../../whats_new.rst:1092 msgid "Fix lambda converters in a non-module context (e.g. ``eval``)." msgstr "モジュール以外での無名コンバーターを修正しました。(例: ``eval`` )" -#: ../../whats_new.rst:1060 +#: ../../whats_new.rst:1093 msgid "Use message creation time for reference time when computing cooldowns." msgstr "クールダウンの計算にメッセージの作成時間を使用するようになりました。" -#: ../../whats_new.rst:1061 +#: ../../whats_new.rst:1094 msgid "This prevents cooldowns from triggering during e.g. a RESUME session." msgstr "これにより、RESUME中でのクールダウンの挙動が修正されました。" -#: ../../whats_new.rst:1062 +#: ../../whats_new.rst:1095 msgid "Fix the default :func:`on_command_error` to work with new-style cogs (:issue:`2094`)" msgstr "新しいスタイルのコグのため、 :func:`on_command_error` のデフォルトの挙動を修正しました。( :issue:`2094` )" -#: ../../whats_new.rst:1063 +#: ../../whats_new.rst:1096 msgid "DM channels are now recognised as NSFW in :func:`~.commands.is_nsfw` check." msgstr "DMチャンネルが :func:`~.commands.is_nsfw` に認識されるようになりました。" -#: ../../whats_new.rst:1064 +#: ../../whats_new.rst:1097 msgid "Fix race condition with help commands (:issue:`2123`)" msgstr "ヘルプコマンドの競合状態を修正しました。 (:issue:`2123`)" -#: ../../whats_new.rst:1065 +#: ../../whats_new.rst:1098 msgid "Fix cog descriptions not showing in :class:`~.commands.MinimalHelpCommand` (:issue:`2139`)" msgstr ":class:`~.commands.MinimalHelpCommand` にコグの説明が表示されるようになりました。( :issue:`2139` )" -#: ../../whats_new.rst:1070 +#: ../../whats_new.rst:1103 msgid "Improve the performance of internal enum creation in the library by about 5x." msgstr "ライブラリ内での列挙型の作成が約5倍早くなりました。" -#: ../../whats_new.rst:1071 +#: ../../whats_new.rst:1104 msgid "Make the output of ``python -m discord --version`` a bit more useful." msgstr "``python -m discord --version`` の出力を改善しました。" -#: ../../whats_new.rst:1072 +#: ../../whats_new.rst:1105 msgid "The loop cleanup facility has been rewritten again." msgstr "ループのクリーンアップがまた書き直されました。" -#: ../../whats_new.rst:1073 +#: ../../whats_new.rst:1106 msgid "The signal handling in :meth:`Client.run` has been removed." msgstr ":meth:`Client.run` でのシグナル制御が削除されました。" -#: ../../whats_new.rst:1078 +#: ../../whats_new.rst:1111 msgid "Custom exception classes are now used for all default checks in the library (:issue:`2101`)" msgstr "ライブラリ内の全てのチェックがカスタム例外クラスを使うようになりました( :issue:`2101` )" -#: ../../whats_new.rst:1084 +#: ../../whats_new.rst:1117 msgid "v1.0.1" msgstr "v1.0.1" -#: ../../whats_new.rst:1089 +#: ../../whats_new.rst:1122 msgid "Fix issue with speaking state being cast to ``int`` when it was invalid." msgstr "スピーキング状態が無効なときに ``int`` にキャストした場合に発生する問題を修正しました。" -#: ../../whats_new.rst:1090 +#: ../../whats_new.rst:1123 msgid "Fix some issues with loop cleanup that some users experienced on Linux machines." msgstr "一部のユーザーがLinuxマシンで遭遇したループクリーンアップに関する問題を修正しました。" -#: ../../whats_new.rst:1091 +#: ../../whats_new.rst:1124 msgid "Fix voice handshake race condition (:issue:`2056`, :issue:`2063`)" msgstr "ボイスハンドシェイクの競合状態を修正しました。 (:issue:`2056`, :issue:`2063`)" -#: ../../whats_new.rst:1096 +#: ../../whats_new.rst:1129 msgid "v1.0.0" msgstr "v1.0.0" -#: ../../whats_new.rst:1098 +#: ../../whats_new.rst:1131 msgid "The changeset for this version are too big to be listed here, for more information please see :ref:`the migrating page `." msgstr "このバージョンの変更は大きすぎるため、この場所に収まりきりません。詳細については :ref:`移行についてのページ ` を参照してください。" -#: ../../whats_new.rst:1105 +#: ../../whats_new.rst:1138 msgid "v0.16.6" msgstr "v0.16.6" -#: ../../whats_new.rst:1110 +#: ../../whats_new.rst:1143 msgid "Fix issue with :meth:`Client.create_server` that made it stop working." msgstr ":meth:`Client.create_server` によって動作が停止する問題を修正しました。" -#: ../../whats_new.rst:1111 +#: ../../whats_new.rst:1144 msgid "Fix main thread being blocked upon calling ``StreamPlayer.stop``." msgstr "``StreamPlayer.stop`` の呼び出し時にメインスレッドがブロックされるのを修正しました。" -#: ../../whats_new.rst:1112 +#: ../../whats_new.rst:1145 msgid "Handle HEARTBEAT_ACK and resume gracefully when it occurs." msgstr "HEARTBEAT_ACKを処理し、正常に再開します。" -#: ../../whats_new.rst:1113 +#: ../../whats_new.rst:1146 msgid "Fix race condition when pre-emptively rate limiting that caused releasing an already released lock." msgstr "既に開放されているロックを解放しようとする原因になっていた先制的なレート制限を行っている時の競合状態を修正しました。" -#: ../../whats_new.rst:1114 +#: ../../whats_new.rst:1147 msgid "Fix invalid state errors when immediately cancelling a coroutine." msgstr "コルーチンを直ちにキャンセルするときに無効な状態になるエラーを修正しました。" -#: ../../whats_new.rst:1119 +#: ../../whats_new.rst:1152 msgid "v0.16.1" msgstr "v0.16.1" -#: ../../whats_new.rst:1121 +#: ../../whats_new.rst:1154 msgid "This release is just a bug fix release with some better rate limit implementation." msgstr "このリリースはバグ修正であり、いくつかのレート制限の実装が改善されています。" -#: ../../whats_new.rst:1126 +#: ../../whats_new.rst:1159 msgid "Servers are now properly chunked for user bots." msgstr "ユーザーボットがサーバーを適切にチャンクするようにしました。" -#: ../../whats_new.rst:1127 +#: ../../whats_new.rst:1160 msgid "The CDN URL is now used instead of the API URL for assets." msgstr "アセットのAPI URLの代わりにCDN URLが使用されるようになりました。" -#: ../../whats_new.rst:1128 +#: ../../whats_new.rst:1161 msgid "Rate limit implementation now tries to use header information if possible." msgstr "レート制限の実装が可能な場合ヘッダ情報を利用するようにしました。" -#: ../../whats_new.rst:1129 +#: ../../whats_new.rst:1162 msgid "Event loop is now properly propagated (:issue:`420`)" msgstr "イベントループが正しく伝播するようにしました。 (:issue:`420`)" -#: ../../whats_new.rst:1130 +#: ../../whats_new.rst:1163 msgid "Allow falsey values in :meth:`Client.send_message` and :meth:`Client.send_file`." msgstr ":meth:`Client.send_message` と :meth:`Client.send_file` でFalseに変換される値を利用できるようにしました。" -#: ../../whats_new.rst:1135 +#: ../../whats_new.rst:1168 msgid "v0.16.0" msgstr "v0.16.0" -#: ../../whats_new.rst:1140 +#: ../../whats_new.rst:1173 msgid "Add :attr:`Channel.overwrites` to get all the permission overwrites of a channel." msgstr "チャンネルの権限上書きをすべて取得する :attr:`Channel.overwrites` を追加しました。" -#: ../../whats_new.rst:1141 +#: ../../whats_new.rst:1174 msgid "Add :attr:`Server.features` to get information about partnered servers." msgstr "パートナーサーバーの情報を得ることのできる :attr:`Server.features` を追加しました。" -#: ../../whats_new.rst:1146 +#: ../../whats_new.rst:1179 msgid "Timeout when waiting for offline members while triggering :func:`on_ready`." msgstr ":func:`on_ready` を実行中にオフラインメンバーを待っているとき、タイムアウトするようにしました。" -#: ../../whats_new.rst:1148 +#: ../../whats_new.rst:1181 msgid "The fact that we did not timeout caused a gigantic memory leak in the library that caused thousands of duplicate :class:`Member` instances causing big memory spikes." msgstr "以前はタイムアウトしなかったため、ライブラリで数千もの :class:`Member` インスタンスが作成されメモリ使用量が大幅に上昇する大規模なメモリリークが発生していました。" -#: ../../whats_new.rst:1151 +#: ../../whats_new.rst:1184 msgid "Discard null sequences in the gateway." msgstr "ゲートウェイでヌル値のシーケンスを破棄するようにしました。" -#: ../../whats_new.rst:1153 +#: ../../whats_new.rst:1186 msgid "The fact these were not discarded meant that :func:`on_ready` kept being called instead of :func:`on_resumed`. Since this has been corrected, in most cases :func:`on_ready` will be called once or twice with :func:`on_resumed` being called much more often." msgstr "以前は破棄されていなかったため、 :func:`on_ready` が :func:`on_resumed` の代わりに呼び出されることがありました。これが修正されたため、多くの場合では :func:`on_ready` は一、二回呼び出されるだけで、 :func:`on_resumed` がより頻繁に呼び出されるようになります。" -#: ../../whats_new.rst:1160 +#: ../../whats_new.rst:1193 msgid "v0.15.1" msgstr "v0.15.1" -#: ../../whats_new.rst:1162 +#: ../../whats_new.rst:1195 msgid "Fix crash on duplicate or out of order reactions." msgstr "重複したり、順番になっていないリアクションによるクラッシュを修正しました。" -#: ../../whats_new.rst:1167 +#: ../../whats_new.rst:1200 msgid "v0.15.0" msgstr "v0.15.0" -#: ../../whats_new.rst:1172 +#: ../../whats_new.rst:1205 msgid "Rich Embeds for messages are now supported." msgstr "メッセージのリッチな埋め込みをサポートするようにしました。" -#: ../../whats_new.rst:1174 +#: ../../whats_new.rst:1207 msgid "To do so, create your own :class:`Embed` and pass the instance to the ``embed`` keyword argument to :meth:`Client.send_message` or :meth:`Client.edit_message`." msgstr "このためには、自分の :class:`Embed` を作成してインスタンスを :meth:`Client.send_message` や :meth:`Client.edit_message` の ``embed`` キーワード引数に渡してください。" -#: ../../whats_new.rst:1175 +#: ../../whats_new.rst:1208 msgid "Add :meth:`Client.clear_reactions` to remove all reactions from a message." msgstr "メッセージからすべてリアクションを除去する :meth:`Client.clear_reactions` を追加しました。" -#: ../../whats_new.rst:1176 +#: ../../whats_new.rst:1209 msgid "Add support for MESSAGE_REACTION_REMOVE_ALL event, under :func:`on_reaction_clear`." msgstr ":func:`on_reaction_clear` の下にMESSAGE_REMOVE_ALL イベントのサポートを追加しました。" -#: ../../whats_new.rst:1177 +#: ../../whats_new.rst:1210 msgid "Add :meth:`Permissions.update` and :meth:`PermissionOverwrite.update` for bulk permission updates." msgstr "一括して権限を更新する、 :meth:`Permissions.update` と :meth:`PermissionOverwrite.update` を追加しました。" -#: ../../whats_new.rst:1179 +#: ../../whats_new.rst:1212 msgid "This allows you to use e.g. ``p.update(read_messages=True, send_messages=False)`` in a single line." msgstr "これにより、例えば ``p.update(read_messages=True, send_messages=False)`` のように一行で使用できます。" -#: ../../whats_new.rst:1180 +#: ../../whats_new.rst:1213 msgid "Add :meth:`PermissionOverwrite.is_empty` to check if the overwrite is empty (i.e. has no overwrites set explicitly as true or false)." msgstr "権限上書きが空か(すなわち、明示的にtrueまたはfalseに設定されている上書きが存在しないか)を確認する :meth:`PermissionOverwrite.is_empty` を追加しました。" -#: ../../whats_new.rst:1182 +#: ../../whats_new.rst:1215 msgid "For the command extension, the following changed:" msgstr "コマンド拡張の場合、以下のことが変更されます。" -#: ../../whats_new.rst:1184 +#: ../../whats_new.rst:1217 msgid "``Context`` is no longer slotted to facilitate setting dynamic attributes." msgstr "``Context`` への動的属性の設定を容易にするためにスロット制限を除去しました。" -#: ../../whats_new.rst:1189 +#: ../../whats_new.rst:1222 msgid "v0.14.3" msgstr "v0.14.3" -#: ../../whats_new.rst:1194 +#: ../../whats_new.rst:1227 msgid "Fix crash when dealing with MESSAGE_REACTION_REMOVE" msgstr "MESSAGE_REACTION_REMOVEを扱う際のクラッシュを修正しました" -#: ../../whats_new.rst:1195 +#: ../../whats_new.rst:1228 msgid "Fix incorrect buckets for reactions." msgstr "リアクションに誤ったバケットが適用されていたのを修正しました。" -#: ../../whats_new.rst:1200 +#: ../../whats_new.rst:1233 msgid "v0.14.2" msgstr "v0.14.2" -#: ../../whats_new.rst:1206 +#: ../../whats_new.rst:1239 msgid ":meth:`Client.wait_for_reaction` now returns a namedtuple with ``reaction`` and ``user`` attributes." msgstr ":meth:`Client.wait_for_reaction` が ``reaction`` と ``user`` 属性を持つ名前付きタプルを返すようになりました。" -#: ../../whats_new.rst:1206 +#: ../../whats_new.rst:1239 msgid "This is for better support in the case that ``None`` is returned since tuple unpacking can lead to issues." msgstr "これは、タプルを展開すると問題につながる可能性がある、 ``None`` が返された場合のより良いサポートのためです。" -#: ../../whats_new.rst:1211 +#: ../../whats_new.rst:1244 msgid "Fix bug that disallowed ``None`` to be passed for ``emoji`` parameter in :meth:`Client.wait_for_reaction`." msgstr ":meth:`Client.wait_for_reaction` の ``emoji`` パラメータに ``None`` を渡すことを許可しないバグを修正しました。" -#: ../../whats_new.rst:1216 +#: ../../whats_new.rst:1249 msgid "v0.14.1" msgstr "v0.14.1" -#: ../../whats_new.rst:1219 +#: ../../whats_new.rst:1252 msgid "Bug fixes" msgstr "バグ修正" -#: ../../whats_new.rst:1222 +#: ../../whats_new.rst:1255 msgid "Fix bug with ``Reaction`` not being visible at import." msgstr "インポート時に ``Reaction`` が表示されないバグを修正しました。" -#: ../../whats_new.rst:1222 +#: ../../whats_new.rst:1255 msgid "This was also breaking the documentation." msgstr "これは、ドキュメントにも影響を与えていました。" -#: ../../whats_new.rst:1227 +#: ../../whats_new.rst:1260 msgid "v0.14.0" msgstr "v0.14.0" -#: ../../whats_new.rst:1229 +#: ../../whats_new.rst:1262 msgid "This update adds new API features and a couple of bug fixes." msgstr "このアップデートには、新しいAPI機能といくつかのバグ修正が含まれています。" -#: ../../whats_new.rst:1234 +#: ../../whats_new.rst:1267 msgid "Add support for Manage Webhooks permission under :attr:`Permissions.manage_webhooks`" msgstr ":attr:`Permissions.manage_webhooks` の下にWebhookの管理の権限のサポートを追加しました。" -#: ../../whats_new.rst:1235 +#: ../../whats_new.rst:1268 msgid "Add support for ``around`` argument in 3.5+ :meth:`Client.logs_from`." msgstr "3.5+ :meth:`Client.logs_from` で ``around`` 引数のサポートを追加しました。" -#: ../../whats_new.rst:1243 +#: ../../whats_new.rst:1276 msgid "Add support for reactions." msgstr "リアクションのサポートを追加します。" -#: ../../whats_new.rst:1237 +#: ../../whats_new.rst:1270 msgid ":meth:`Client.add_reaction` to add a reactions" msgstr "リアクションを追加する :meth:`Client.add_reaction`" -#: ../../whats_new.rst:1238 +#: ../../whats_new.rst:1271 msgid ":meth:`Client.remove_reaction` to remove a reaction." msgstr "リアクションを除去する :meth:`Client.remove_reaction`" -#: ../../whats_new.rst:1239 +#: ../../whats_new.rst:1272 msgid ":meth:`Client.get_reaction_users` to get the users that reacted to a message." msgstr "メッセージにリアクションしたユーザーを取得する :meth:`Client.get_reaction_users`" -#: ../../whats_new.rst:1240 +#: ../../whats_new.rst:1273 msgid ":attr:`Permissions.add_reactions` permission bit support." msgstr ":attr:`Permissions.add_reactions` パーミッションビットのサポート。" -#: ../../whats_new.rst:1241 +#: ../../whats_new.rst:1274 msgid "Two new events, :func:`on_reaction_add` and :func:`on_reaction_remove`." msgstr "2つの新しいイベント、 :func:`on_reaction_add` と :func:`on_reaction_remove` 。" -#: ../../whats_new.rst:1242 +#: ../../whats_new.rst:1275 msgid ":attr:`Message.reactions` to get reactions from a message." msgstr "メッセージからリアクションを取得する :attr:`Message.reactions`" -#: ../../whats_new.rst:1243 +#: ../../whats_new.rst:1276 msgid ":meth:`Client.wait_for_reaction` to wait for a reaction from a user." msgstr "ユーザーからのリアクションを待つ :meth:`Client.wait_for_reaction`" -#: ../../whats_new.rst:1248 +#: ../../whats_new.rst:1281 msgid "Fix bug with Paginator still allowing lines that are too long." msgstr "Paginatorが長すぎる行をいまだ許可していたバグを修正しました。" -#: ../../whats_new.rst:1249 +#: ../../whats_new.rst:1282 msgid "Fix the :attr:`Permissions.manage_emojis` bit being incorrect." msgstr ":attr:`Permissions.manage_emojis` ビットが正しくないバグを修正しました。" -#: ../../whats_new.rst:1254 +#: ../../whats_new.rst:1287 msgid "v0.13.0" msgstr "v0.13.0" -#: ../../whats_new.rst:1256 +#: ../../whats_new.rst:1289 msgid "This is a backwards compatible update with new features." msgstr "これは、新しい機能を備えた後方互換性のあるアップデートです。" -#: ../../whats_new.rst:1261 +#: ../../whats_new.rst:1294 msgid "Add the ability to manage emojis." msgstr "絵文字を管理する機能を追加しました。" -#: ../../whats_new.rst:1263 +#: ../../whats_new.rst:1296 msgid ":meth:`Client.create_custom_emoji` to create new emoji." msgstr "新しい絵文字を作成する :meth:`Client.create_custom_emoji` 。" -#: ../../whats_new.rst:1264 +#: ../../whats_new.rst:1297 msgid ":meth:`Client.edit_custom_emoji` to edit an old emoji." msgstr "既存の絵文字を編集する :meth:`Client.edit_custom_emoji` 。" -#: ../../whats_new.rst:1265 +#: ../../whats_new.rst:1298 msgid ":meth:`Client.delete_custom_emoji` to delete a custom emoji." msgstr "カスタム絵文字を削除する :meth:`Client.delete_custom_emoji` 。" -#: ../../whats_new.rst:1266 +#: ../../whats_new.rst:1299 msgid "Add new :attr:`Permissions.manage_emojis` toggle." msgstr "新しい :attr:`Permissions.manage_emoji` トグルを追加しました。" -#: ../../whats_new.rst:1268 +#: ../../whats_new.rst:1301 msgid "This applies for :class:`PermissionOverwrite` as well." msgstr "これは :class:`PermissionOverwrite` にも適用されます。" -#: ../../whats_new.rst:1269 +#: ../../whats_new.rst:1302 msgid "Add new statuses for :class:`Status`." msgstr ":class:`Status` に新しいステータスを追加しました。" -#: ../../whats_new.rst:1271 +#: ../../whats_new.rst:1304 msgid ":attr:`Status.dnd` (aliased with :attr:`Status.do_not_disturb`\\) for Do Not Disturb." msgstr "取り込み中を示す :attr:`Status.dnd` (エイリアス :attr:`Status.do_not_interrup` )" -#: ../../whats_new.rst:1272 +#: ../../whats_new.rst:1305 msgid ":attr:`Status.invisible` for setting your status to invisible (please see the docs for a caveat)." msgstr "ステータスを非表示に設定するための :attr:`Status.invisible` (ドキュメントの注意事項を参照してください)。" -#: ../../whats_new.rst:1273 +#: ../../whats_new.rst:1306 msgid "Deprecate :meth:`Client.change_status`" msgstr ":meth:`Client.change_status` を非推奨にしました。" -#: ../../whats_new.rst:1275 +#: ../../whats_new.rst:1308 msgid "Use :meth:`Client.change_presence` instead for better more up to date functionality." msgstr "より良い最新の機能を使用するためには、 :meth:`Client.change_presence` を使用してください。" -#: ../../whats_new.rst:1276 +#: ../../whats_new.rst:1309 msgid "This method is subject for removal in a future API version." msgstr "このメソッドは、将来の API バージョンで削除の対象となります。" -#: ../../whats_new.rst:1277 +#: ../../whats_new.rst:1310 msgid "Add :meth:`Client.change_presence` for changing your status with the new Discord API change." msgstr "新しい Discord API でステータスを変更するための :meth:`Client.change_presence` を追加しました。" -#: ../../whats_new.rst:1279 +#: ../../whats_new.rst:1312 msgid "This is the only method that allows changing your status to invisible or do not disturb." msgstr "これは、ステータスを非表示や取り込み中に変更できる唯一の方法です。" -#: ../../whats_new.rst:1284 +#: ../../whats_new.rst:1317 msgid "Paginator pages do not exceed their max_size anymore (:issue:`340`)" msgstr "ページネータのページがmax_sizeを超えないようにしました。 (:issue:`340`)" -#: ../../whats_new.rst:1285 +#: ../../whats_new.rst:1318 msgid "Do Not Disturb users no longer show up offline due to the new :class:`Status` changes." msgstr "取り込み中ユーザーは新しい :class:`Status` の変更によりこれ以降オフラインとして表示されないようになりました。" -#: ../../whats_new.rst:1290 +#: ../../whats_new.rst:1323 msgid "v0.12.0" msgstr "v0.12.0" -#: ../../whats_new.rst:1292 +#: ../../whats_new.rst:1325 msgid "This is a bug fix update that also comes with new features." msgstr "これは、新機能つきのバグ修正アップデートです。" -#: ../../whats_new.rst:1297 +#: ../../whats_new.rst:1330 msgid "Add custom emoji support." msgstr "カスタム絵文字サポートを追加しました。" -#: ../../whats_new.rst:1299 +#: ../../whats_new.rst:1332 msgid "Adds a new class to represent a custom Emoji named :class:`Emoji`" msgstr ":class:`Emoji` という名前のカスタム絵文字を表す新しいクラスを追加しました。" -#: ../../whats_new.rst:1300 +#: ../../whats_new.rst:1333 msgid "Adds a utility generator function, :meth:`Client.get_all_emojis`." msgstr "ユーティリティジェネレータ関数 :meth:`Client.get_all_emojis` を追加しました。" -#: ../../whats_new.rst:1301 +#: ../../whats_new.rst:1334 msgid "Adds a list of emojis on a server, :attr:`Server.emojis`." msgstr "サーバーの絵文字のリストを取得する :attr:`Server.emojis` を追加しました。" -#: ../../whats_new.rst:1302 +#: ../../whats_new.rst:1335 msgid "Adds a new event, :func:`on_server_emojis_update`." msgstr "新しいイベント :func:`on_server_emojis_update` を追加しました。" -#: ../../whats_new.rst:1303 +#: ../../whats_new.rst:1336 msgid "Add new server regions to :class:`ServerRegion`" msgstr ":class:`ServerRegion` に新しいサーバーリージョンを追加しました。" -#: ../../whats_new.rst:1305 +#: ../../whats_new.rst:1338 msgid ":attr:`ServerRegion.eu_central` and :attr:`ServerRegion.eu_west`." msgstr ":attr:`ServerRegion.eu_central` と :attr:`ServerRegion.eu_west` 。" -#: ../../whats_new.rst:1306 +#: ../../whats_new.rst:1339 msgid "Add support for new pinned system message under :attr:`MessageType.pins_add`." msgstr ":attr:`MessageType.pins_add` にて新しいピン留めのシステムメッセージのサポートを追加しました。" -#: ../../whats_new.rst:1307 +#: ../../whats_new.rst:1340 msgid "Add order comparisons for :class:`Role` to allow it to be compared with regards to hierarchy." msgstr ":class:`Role` への比較を追加し、階層を考慮した比較ができるようにしました。" -#: ../../whats_new.rst:1309 +#: ../../whats_new.rst:1342 msgid "This means that you can now do ``role_a > role_b`` etc to check if ``role_b`` is lower in the hierarchy." msgstr "つまり、 ``role_a > role_b`` などを実行して、階層内で ``role_b`` が低いかどうかを確認できるようになりました。" -#: ../../whats_new.rst:1311 +#: ../../whats_new.rst:1344 msgid "Add :attr:`Server.role_hierarchy` to get the server's role hierarchy." msgstr "サーバーのロール階層を取得する :attr:`Server.role_hierarchy` を追加しました。" -#: ../../whats_new.rst:1312 +#: ../../whats_new.rst:1345 msgid "Add :attr:`Member.server_permissions` to get a member's server permissions without their channel specific overwrites." msgstr "チャンネル固有の上書きなしでメンバーのサーバー権限を取得する :attr:`Member.server_permissions` を追加しました。" -#: ../../whats_new.rst:1313 +#: ../../whats_new.rst:1346 msgid "Add :meth:`Client.get_user_info` to retrieve a user's info from their ID." msgstr "IDからユーザ情報を取得することができる、 :meth:`Client.get_user_info` を追加しました。" -#: ../../whats_new.rst:1314 +#: ../../whats_new.rst:1347 msgid "Add a new ``Player`` property, ``Player.error`` to fetch the error that stopped the player." msgstr "プレイヤーを停止させたエラーを取得するために、新しい ``Player`` プロパティ ``Player.error`` を追加しました。" -#: ../../whats_new.rst:1316 +#: ../../whats_new.rst:1349 msgid "To help with this change, a player's ``after`` function can now take a single parameter denoting the current player." msgstr "この変更とともに、プレイヤーの ``after`` 関数に現在のプレイヤーを示すパラメータを取ることができるようになりました。" -#: ../../whats_new.rst:1317 +#: ../../whats_new.rst:1350 msgid "Add support for server verification levels." msgstr "サーバー認証レベルのサポートを追加しました。" -#: ../../whats_new.rst:1319 +#: ../../whats_new.rst:1352 msgid "Adds a new enum called :class:`VerificationLevel`." msgstr ":class:`VerificationLevel` という新しい列挙型を追加しました。" -#: ../../whats_new.rst:1320 +#: ../../whats_new.rst:1353 msgid "This enum can be used in :meth:`Client.edit_server` under the ``verification_level`` keyword argument." msgstr "この列挙型は、 :meth:`Client.edit_server` の ``verification_level`` キーワード引数で使用できます。" -#: ../../whats_new.rst:1321 +#: ../../whats_new.rst:1354 msgid "Adds a new attribute in the server, :attr:`Server.verification_level`." msgstr "サーバーに :attr:`Server.verification_level` という新しい属性を追加しました。" -#: ../../whats_new.rst:1322 +#: ../../whats_new.rst:1355 msgid "Add :attr:`Server.voice_client` shortcut property for :meth:`Client.voice_client_in`." msgstr ":meth:`Client.voice_client_in` のショートカットプロパティである :attr:`Server.voice_client` を追加しました。" -#: ../../whats_new.rst:1324 +#: ../../whats_new.rst:1357 msgid "This is technically old (was added in v0.10.0) but was undocumented until v0.12.0." msgstr "これは厳密にいえば過去のもの (v0.10.0で追加) ですが、v0.12.0までは文書化されていませんでした。" -#: ../../whats_new.rst:1326 -#: ../../whats_new.rst:1372 +#: ../../whats_new.rst:1359 +#: ../../whats_new.rst:1405 msgid "For the command extension, the following are new:" msgstr "コマンド拡張機能では、以下の新機能が追加されました:" -#: ../../whats_new.rst:1328 +#: ../../whats_new.rst:1361 msgid "Add custom emoji converter." msgstr "カスタム絵文字コンバータを追加しました。" -#: ../../whats_new.rst:1329 +#: ../../whats_new.rst:1362 msgid "All default converters that can take IDs can now convert via ID." msgstr "IDを取ることができるすべてのデフォルトのコンバータが、IDにより変換することができるようにしました。" -#: ../../whats_new.rst:1330 +#: ../../whats_new.rst:1363 msgid "Add coroutine support for ``Bot.command_prefix``." msgstr "``Bot.command_prefix`` にコルーチンサポートを追加しました。" -#: ../../whats_new.rst:1331 +#: ../../whats_new.rst:1364 msgid "Add a method to reset command cooldown." msgstr "コマンドのクールダウンをリセットするメソッドを追加しました。" -#: ../../whats_new.rst:1336 +#: ../../whats_new.rst:1369 msgid "Fix bug that caused the library to not work with the latest ``websockets`` library." msgstr "最新の ``websockets`` ライブラリでライブラリが動作しないバグを修正しました。" -#: ../../whats_new.rst:1337 +#: ../../whats_new.rst:1370 msgid "Fix bug that leaked keep alive threads (:issue:`309`)" msgstr "キープアライブスレッドをリークしていたバグを修正しました。 (:issue:`309`)" -#: ../../whats_new.rst:1338 +#: ../../whats_new.rst:1371 msgid "Fix bug that disallowed :class:`ServerRegion` from being used in :meth:`Client.edit_server`." msgstr ":meth:`Client.edit_server` で :class:`ServerRegion` が使用できないバグを修正しました。" -#: ../../whats_new.rst:1339 +#: ../../whats_new.rst:1372 msgid "Fix bug in :meth:`Channel.permissions_for` that caused permission resolution to happen out of order." msgstr ":meth:`Channel.permissions_for` で権限解決が誤った順序で行われたバグを修正しました。" -#: ../../whats_new.rst:1340 +#: ../../whats_new.rst:1373 msgid "Fix bug in :attr:`Member.top_role` that did not account for same-position roles." msgstr ":attr:`Member.top_role` が同じポジションの役割を考慮しないバグを修正しました。" -#: ../../whats_new.rst:1345 +#: ../../whats_new.rst:1378 msgid "v0.11.0" msgstr "v0.11.0" -#: ../../whats_new.rst:1347 +#: ../../whats_new.rst:1380 msgid "This is a minor bug fix update that comes with a gateway update (v5 -> v6)." msgstr "これはゲートウェイのアップデート (v5 -> v6) を含むマイナーなバグ修正アップデートです。" -#: ../../whats_new.rst:1350 +#: ../../whats_new.rst:1383 msgid "Breaking Changes" msgstr "破壊的変更" -#: ../../whats_new.rst:1352 +#: ../../whats_new.rst:1385 msgid "``Permissions.change_nicknames`` has been renamed to :attr:`Permissions.change_nickname` to match the UI." msgstr "``Permissions.change_nicknames`` は UIに一致するように :attr:`Permissions.change_nickname` に名前が変更されました。" -#: ../../whats_new.rst:1357 +#: ../../whats_new.rst:1390 msgid "Add the ability to prune members via :meth:`Client.prune_members`." msgstr ":meth:`Client.prune_members` でメンバーを一括キックする機能を追加しました。" -#: ../../whats_new.rst:1358 +#: ../../whats_new.rst:1391 msgid "Switch the websocket gateway version to v6 from v5. This allows the library to work with group DMs and 1-on-1 calls." msgstr "WebSocketゲートウェイのバージョンをv5からv6に切り替えました。これにより、ライブラリはグループDMと1-on-1コールで動作するようになります。" -#: ../../whats_new.rst:1359 +#: ../../whats_new.rst:1392 msgid "Add :attr:`AppInfo.owner` attribute." msgstr ":attr:`AppInfo.owner` 属性を追加しました。" -#: ../../whats_new.rst:1360 +#: ../../whats_new.rst:1393 msgid "Add :class:`CallMessage` for group voice call messages." msgstr "グループボイス通話メッセージを示す :class:`CallMessage` を追加しました。" -#: ../../whats_new.rst:1361 +#: ../../whats_new.rst:1394 msgid "Add :class:`GroupCall` for group voice call information." msgstr "グループボイス通話情報を示す :class:`GroupCall` を追加しました。" -#: ../../whats_new.rst:1362 +#: ../../whats_new.rst:1395 msgid "Add :attr:`Message.system_content` to get the system message." msgstr "システムメッセージを取得する :attr:`Message.system_content` を追加しました。" -#: ../../whats_new.rst:1363 +#: ../../whats_new.rst:1396 msgid "Add the remaining VIP servers and the Brazil servers into :class:`ServerRegion` enum." msgstr "残りのVIPサーバーとブラジルサーバーを :class:`ServerRegion` に追加しました。" -#: ../../whats_new.rst:1364 +#: ../../whats_new.rst:1397 msgid "Add ``stderr`` argument to :meth:`VoiceClient.create_ffmpeg_player` to redirect stderr." msgstr ":meth:`VoiceClient.create_ffmpeg_player` に標準エラー出力をリダイレクトするための ``stderr`` 引数を追加しました。" -#: ../../whats_new.rst:1365 +#: ../../whats_new.rst:1398 msgid "The library now handles implicit permission resolution in :meth:`Channel.permissions_for`." msgstr "ライブラリは :meth:`Channel.permissions_for` で暗黙的な権限解決を処理するようになりました。" -#: ../../whats_new.rst:1366 +#: ../../whats_new.rst:1399 msgid "Add :attr:`Server.mfa_level` to query a server's 2FA requirement." msgstr "サーバーの 2FA 要件を取得する :attr:`Server.mfa_level` を追加しました。" -#: ../../whats_new.rst:1367 +#: ../../whats_new.rst:1400 msgid "Add :attr:`Permissions.external_emojis` permission." msgstr ":attr:`Permissions.external_emojis` 権限を追加しました。" -#: ../../whats_new.rst:1368 +#: ../../whats_new.rst:1401 msgid "Add :attr:`Member.voice` attribute that refers to a :class:`VoiceState`." msgstr ":class:`VoiceState` を返す :attr:`Member.voice` 属性を追加しました。" -#: ../../whats_new.rst:1370 +#: ../../whats_new.rst:1403 msgid "For backwards compatibility, the member object will have properties mirroring the old behaviour." msgstr "後方互換性のため、メンバーオブジェクトには古い挙動をミラーリングするプロパティも存在します。" -#: ../../whats_new.rst:1374 +#: ../../whats_new.rst:1407 msgid "Command cooldown system with the ``cooldown`` decorator." msgstr "``cololdown`` デコレータを用いたコマンドクールダウンシステム。" -#: ../../whats_new.rst:1375 +#: ../../whats_new.rst:1408 msgid "``UserInputError`` exception for the hierarchy for user input related errors." msgstr "ユーザー入力関連エラーの親である ``UserInputError`` 例外。" -#: ../../whats_new.rst:1380 +#: ../../whats_new.rst:1413 msgid ":attr:`Client.email` is now saved when using a token for user accounts." msgstr ":attr:`Client.email` がユーザーアカウントにトークンを使用してログインしたとき保存されるようになりました。" -#: ../../whats_new.rst:1381 +#: ../../whats_new.rst:1414 msgid "Fix issue when removing roles out of order." msgstr "順番になってないロールの除去で発生した問題を修正しました。" -#: ../../whats_new.rst:1382 +#: ../../whats_new.rst:1415 msgid "Fix bug where discriminators would not update." msgstr "タグが更新されないバグを修正しました。" -#: ../../whats_new.rst:1383 +#: ../../whats_new.rst:1416 msgid "Handle cases where ``HEARTBEAT`` opcode is received. This caused bots to disconnect seemingly randomly." msgstr "``HEARTBEAT`` のコードを受け取った場合を処理するようにしました。これは、ボットが一見ランダムに切断されるのを引き起こしていました。" -#: ../../whats_new.rst:1385 +#: ../../whats_new.rst:1418 msgid "For the command extension, the following bug fixes apply:" msgstr "コマンド拡張機能では、以下のバグが修正されました:" -#: ../../whats_new.rst:1387 +#: ../../whats_new.rst:1420 msgid "``Bot.check`` decorator is actually a decorator not requiring parentheses." msgstr "``Bot.check`` デコレータが実際に括弧を必要としないようになりました。" -#: ../../whats_new.rst:1388 +#: ../../whats_new.rst:1421 msgid "``Bot.remove_command`` and ``Group.remove_command`` no longer throw if the command doesn't exist." msgstr "``Bot.remove_command`` と ``Group.remove_command`` が、コマンドが存在しない場合に例外を送出しないようにしました。" -#: ../../whats_new.rst:1389 +#: ../../whats_new.rst:1422 msgid "Command names are no longer forced to be ``lower()``." msgstr "コマンド名は強制的に ``lower()`` されなくなりました。" -#: ../../whats_new.rst:1390 +#: ../../whats_new.rst:1423 msgid "Fix a bug where Member and User converters failed to work in private message contexts." msgstr "MemberとUserのコンバータがプライベートメッセージ内で動かなかったバグを修正しました。" -#: ../../whats_new.rst:1391 +#: ../../whats_new.rst:1424 msgid "``HelpFormatter`` now ignores hidden commands when deciding the maximum width." msgstr "``HelpFormatter`` が最大幅を決めるときに隠されたコマンドを無視するようになりました。" -#: ../../whats_new.rst:1396 +#: ../../whats_new.rst:1429 msgid "v0.10.0" msgstr "v0.10.0" -#: ../../whats_new.rst:1398 +#: ../../whats_new.rst:1431 msgid "For breaking changes, see :ref:`migrating-to-async`. The breaking changes listed there will not be enumerated below. Since this version is rather a big departure from v0.9.2, this change log will be non-exhaustive." msgstr "破壊的変更に関しては、 :ref:`migrating-to-async` を参照してください。そのページで列挙された破壊的変更はここでは述べません。このバージョンがv0.9.2よりかなり大きな変更であるため、変更履歴は完全ではありません。" -#: ../../whats_new.rst:1403 +#: ../../whats_new.rst:1436 msgid "The library is now fully ``asyncio`` compatible, allowing you to write non-blocking code a lot more easily." msgstr "ライブラリが完全に ``asyncio`` に対応するようになり、ノンブロッキングコードをより簡単に書けるようになりました。" -#: ../../whats_new.rst:1404 +#: ../../whats_new.rst:1437 msgid "The library now fully handles 429s and unconditionally retries on 502s." msgstr "ライブラリが429を完全に処理し、502で無条件に再試行するようにしました。" -#: ../../whats_new.rst:1405 +#: ../../whats_new.rst:1438 msgid "A new command extension module was added but is currently undocumented. Figuring it out is left as an exercise to the reader." msgstr "新しいコマンド拡張機能モジュールが追加されましたが、現在文書化されていません。詳細は読者が自身で調べることをおすすめします。" -#: ../../whats_new.rst:1406 +#: ../../whats_new.rst:1439 msgid "Two new exception types, :exc:`Forbidden` and :exc:`NotFound` to denote permission errors or 404 errors." msgstr "パーミッションエラーや404エラーを示す2つの新しい例外タイプ、 :exc:`Forbidden` と :exc:`NotFound` が追加されました。" -#: ../../whats_new.rst:1407 +#: ../../whats_new.rst:1440 msgid "Added :meth:`Client.delete_invite` to revoke invites." msgstr "招待を取り消す :meth:`Client.delete_invite` を追加しました。" -#: ../../whats_new.rst:1408 +#: ../../whats_new.rst:1441 msgid "Added support for sending voice. Check :class:`VoiceClient` for more details." msgstr "音声を送信するためのサポートを追加しました。詳細は :class:`VoiceClient` を参照してください。" -#: ../../whats_new.rst:1409 +#: ../../whats_new.rst:1442 msgid "Added :meth:`Client.wait_for_message` coroutine to aid with follow up commands." msgstr "フォローアップコマンドを作りやすいように、コルーチン :meth:`Client.wait_for_message` を追加しました。" -#: ../../whats_new.rst:1410 +#: ../../whats_new.rst:1443 msgid "Added :data:`version_info` named tuple to check version info of the library." msgstr "ライブラリのバージョン情報を確認するための、namedtuple :data:`version_info` を追加しました。" -#: ../../whats_new.rst:1411 +#: ../../whats_new.rst:1444 msgid "Login credentials are now cached to have a faster login experience. You can disable this by passing in ``cache_auth=False`` when constructing a :class:`Client`." msgstr "ログイン情報をキャッシュすることで、より高速にログインできるようになりました。これを無効にするには、 :class:`Client` を作成する際に ``cache_auth=False`` を渡します。" -#: ../../whats_new.rst:1413 +#: ../../whats_new.rst:1446 msgid "New utility function, :func:`discord.utils.get` to simplify retrieval of items based on attributes." msgstr "新しいユーティリティ関数 :func:`discord.utils.get` は、属性に基づいたアイテムの取得を簡素化します。" -#: ../../whats_new.rst:1414 +#: ../../whats_new.rst:1447 msgid "All data classes now support ``!=``, ``==``, ``hash(obj)`` and ``str(obj)``." msgstr "すべてのデータクラスが ``!=``, ``==``, ``hash(obj)``, ``str(obj)`` をサポートするようになりました" -#: ../../whats_new.rst:1415 +#: ../../whats_new.rst:1448 msgid "Added :meth:`Client.get_bans` to get banned members from a server." msgstr "サーバーからBANされたメンバーを取得する :meth:`Client.get_bans` を追加しました。" -#: ../../whats_new.rst:1416 +#: ../../whats_new.rst:1449 msgid "Added :meth:`Client.invites_from` to get currently active invites in a server." msgstr "サーバーで現在アクティブな招待を取得する :meth:`Client.invites_from` を追加しました。" -#: ../../whats_new.rst:1417 +#: ../../whats_new.rst:1450 msgid "Added :attr:`Server.me` attribute to get the :class:`Member` version of :attr:`Client.user`." msgstr ":attr:`Client.user` の :class:`Member` を取得できる :attr:`Server.me` を追加しました。" -#: ../../whats_new.rst:1418 +#: ../../whats_new.rst:1451 msgid "Most data classes now support a ``hash(obj)`` function to allow you to use them in ``set`` or ``dict`` classes or subclasses." msgstr "ほとんどのデータクラスが ``hash(obj)`` 関数をサポートするようになり、 ``set`` や ``dict`` クラス、サブクラスで使用できるようになりました。" -#: ../../whats_new.rst:1419 +#: ../../whats_new.rst:1452 msgid "Add :meth:`Message.clean_content` to get a text version of the content with the user and channel mentioned changed into their names." msgstr "ユーザーとチャンネルのメンションを名前に変更したバージョンのコンテンツを取得する、 :meth:`Message.clean_content` を追加しました。" -#: ../../whats_new.rst:1420 +#: ../../whats_new.rst:1453 msgid "Added a way to remove the messages of the user that just got banned in :meth:`Client.ban`." msgstr ":meth:`Client.ban` でBANされたユーザーのメッセージを削除する方法を追加しました。" -#: ../../whats_new.rst:1421 +#: ../../whats_new.rst:1454 msgid "Added :meth:`Client.wait_until_ready` to facilitate easy creation of tasks that require the client cache to be ready." msgstr "クライアントキャッシュを準備する必要があるタスクを簡単に作成できるように、 :meth:`Client.wait_until_ready` を追加しました。" -#: ../../whats_new.rst:1422 +#: ../../whats_new.rst:1455 msgid "Added :meth:`Client.wait_until_login` to facilitate easy creation of tasks that require the client to be logged in." msgstr "クライアントのログインを必要とするタスクを簡単に作成できるように :meth:`Client.wait_until_login` を追加しました。" -#: ../../whats_new.rst:1423 +#: ../../whats_new.rst:1456 msgid "Add :class:`discord.Game` to represent any game with custom text to send to :meth:`Client.change_status`." msgstr ":class:`Client.change_status` に送信する、カスタムテキストを含む任意のゲームを表す :meth:`discord.Game` を追加しました。" -#: ../../whats_new.rst:1424 +#: ../../whats_new.rst:1457 msgid "Add :attr:`Message.nonce` attribute." msgstr ":attr:`Message.nonce` 属性を追加しました。" -#: ../../whats_new.rst:1425 +#: ../../whats_new.rst:1458 msgid "Add :meth:`Member.permissions_in` as another way of doing :meth:`Channel.permissions_for`." msgstr ":meth:`Channel.permissions_for` の代替として :meth:`Member.permissions_in` を追加しました。" -#: ../../whats_new.rst:1426 +#: ../../whats_new.rst:1459 msgid "Add :meth:`Client.move_member` to move a member to another voice channel." msgstr "メンバーを別のボイスチャンネルに移動するための :meth:`Client.move_member` を追加しました。" -#: ../../whats_new.rst:1427 +#: ../../whats_new.rst:1460 msgid "You can now create a server via :meth:`Client.create_server`." msgstr ":meth:`Client.create_server` を使用してサーバーを作成できるようになりました。" -#: ../../whats_new.rst:1428 +#: ../../whats_new.rst:1461 msgid "Added :meth:`Client.edit_server` to edit existing servers." msgstr "既存のサーバを編集するための :meth:`Client.edit_server` を追加しました。" -#: ../../whats_new.rst:1429 +#: ../../whats_new.rst:1462 msgid "Added :meth:`Client.server_voice_state` to server mute or server deafen a member." msgstr "メンバーをサーバーミュートしたり、サーバースピーカーミュートしたりできる :meth:`Client.server_voice_state` を追加しました。" -#: ../../whats_new.rst:1430 +#: ../../whats_new.rst:1463 msgid "If you are being rate limited, the library will now handle it for you." msgstr "レートリミットの際にライブラリが処理するようになりました。" -#: ../../whats_new.rst:1431 +#: ../../whats_new.rst:1464 msgid "Add :func:`on_member_ban` and :func:`on_member_unban` events that trigger when a member is banned/unbanned." msgstr "メンバーがBANまたはBAN解除されたときに実行される :func:`on_member_ban` と :func:`on_member_unban` イベントを追加しました。" -#: ../../whats_new.rst:1434 +#: ../../whats_new.rst:1467 msgid "Performance Improvements" msgstr "パフォーマンスの改善" -#: ../../whats_new.rst:1436 +#: ../../whats_new.rst:1469 msgid "All data classes now use ``__slots__`` which greatly reduce the memory usage of things kept in cache." msgstr "すべてのデータクラスは ``__slots__`` を使用するようになり、キャッシュに保存されているもののメモリ使用量を大幅に削減しました。" -#: ../../whats_new.rst:1437 +#: ../../whats_new.rst:1470 msgid "Due to the usage of ``asyncio``, the CPU usage of the library has gone down significantly." msgstr "``asyncio`` の使用により、ライブラリの CPU 使用率は大幅に減少しました。" -#: ../../whats_new.rst:1438 +#: ../../whats_new.rst:1471 msgid "A lot of the internal cache lists were changed into dictionaries to change the ``O(n)`` lookup into ``O(1)``." msgstr "多くの内部キャッシュリストが ``O(n)`` 検索を ``O(1)`` に変更するために辞書型に変更されました。" -#: ../../whats_new.rst:1439 +#: ../../whats_new.rst:1472 msgid "Compressed READY is now on by default. This means if you're on a lot of servers (or maybe even a few) you would receive performance improvements by having to download and process less data." msgstr "圧縮されたREADYがデフォルトでオンになりました。 つまり、多くのサーバー(あるいはもしかすると少なめのサーバー) にいる場合、より少ないデータをダウンロードして処理することでパフォーマンスが向上されます。" -#: ../../whats_new.rst:1441 +#: ../../whats_new.rst:1474 msgid "While minor, change regex from ``\\d+`` to ``[0-9]+`` to avoid unnecessary unicode character lookups." msgstr "小規模ながら、不要な Unicode 文字の検索を避けるために正規表現を ``\\d+`` から ``[0-9]+`` に変更しました。" -#: ../../whats_new.rst:1446 +#: ../../whats_new.rst:1479 msgid "Fix bug where guilds being updated did not edit the items in cache." msgstr "ギルドが更新されてもキャッシュ内のアイテムが編集されなかったバグを修正しました。" -#: ../../whats_new.rst:1447 +#: ../../whats_new.rst:1480 msgid "Fix bug where ``member.roles`` were empty upon joining instead of having the ``@everyone`` role." msgstr "``member.roles`` が参加時に ``@everyone`` ロールを有さず空であったバグを修正しました。" -#: ../../whats_new.rst:1448 +#: ../../whats_new.rst:1481 msgid "Fix bug where :meth:`Role.is_everyone` was not being set properly when the role was being edited." msgstr "ロールが編集されたときに :meth:`Role.is_everyone` が正しく設定されていないバグを修正しました。" -#: ../../whats_new.rst:1449 +#: ../../whats_new.rst:1482 msgid ":meth:`Client.logs_from` now handles cases where limit > 100 to sidestep the discord API limitation." msgstr ":meth:`Client.logs_from` が、DiscordのAPI制限を避けるために制限 > 100を超える場合を処理するようになりました。" -#: ../../whats_new.rst:1450 +#: ../../whats_new.rst:1483 msgid "Fix bug where a role being deleted would trigger a ``ValueError``." msgstr "ロールが削除されると、 ``ValueError`` が発生するバグを修正しました。" -#: ../../whats_new.rst:1451 +#: ../../whats_new.rst:1484 msgid "Fix bug where :meth:`Permissions.kick_members` and :meth:`Permissions.ban_members` were flipped." msgstr ":meth:`Permissions.kick_members` と :meth:`Permissions.ban_members` がひっくり返されたバグを修正しました。" -#: ../../whats_new.rst:1452 +#: ../../whats_new.rst:1485 msgid "Mentions are now triggered normally. This was changed due to the way discord handles it internally." msgstr "メンションが正常に発動されるようになりました。これは、Discordの内部処理の方法の変更によるものです。" -#: ../../whats_new.rst:1453 +#: ../../whats_new.rst:1486 msgid "Fix issue when a :class:`Message` would attempt to upgrade a :attr:`Message.server` when the channel is a :class:`Object`." msgstr "チャンネルが :class:`Object` の時に、 :class:`Message` が :attr:`Message.server` をアップグレードしようとする問題を修正しました。" -#: ../../whats_new.rst:1455 +#: ../../whats_new.rst:1488 msgid "Unavailable servers were not being added into cache, this has been corrected." msgstr "利用できないサーバーがキャッシュに追加されない不具合が修正されました。" From 0ee1794c6de7bd61e8fca6bd0b98211b93e331c3 Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Sun, 5 May 2024 05:23:59 +0200 Subject: [PATCH 032/354] Fix channel deletion not removing associated threads --- discord/guild.py | 9 +++++---- discord/raw_models.py | 16 ++++++++++++++++ discord/state.py | 6 ++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index c038188c8bec..6d1c5bde31af 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -378,10 +378,11 @@ def _remove_thread(self, thread: Snowflake, /) -> None: def _clear_threads(self) -> None: self._threads.clear() - def _remove_threads_by_channel(self, channel_id: int) -> None: - to_remove = [k for k, t in self._threads.items() if t.parent_id == channel_id] - for k in to_remove: - del self._threads[k] + def _remove_threads_by_channel(self, channel_id: int) -> List[Thread]: + to_remove = [t for t in self._threads.values() if t.parent_id == channel_id] + for thread in to_remove: + del self._threads[thread.id] + return to_remove def _filter_threads(self, channel_ids: Set[int]) -> Dict[int, Thread]: to_remove: Dict[int, Thread] = {k: t for k, t in self._threads.items() if t.parent_id in channel_ids} diff --git a/discord/raw_models.py b/discord/raw_models.py index 74382b3f871b..2fd94539e1a6 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -33,6 +33,8 @@ from .colour import Colour if TYPE_CHECKING: + from typing_extensions import Self + from .types.gateway import ( MessageDeleteEvent, MessageDeleteBulkEvent as BulkMessageDeleteEvent, @@ -399,6 +401,20 @@ def __init__(self, data: ThreadDeleteEvent) -> None: self.parent_id: int = int(data['parent_id']) self.thread: Optional[Thread] = None + @classmethod + def _from_thread(cls, thread: Thread) -> Self: + data: ThreadDeleteEvent = { + 'id': thread.id, + 'type': thread.type.value, + 'guild_id': thread.guild.id, + 'parent_id': thread.parent_id, + } + + instance = cls(data) + instance.thread = thread + + return instance + class RawThreadMembersUpdate(_RawReprMixin): """Represents the payload for a :func:`on_raw_thread_member_remove` event. diff --git a/discord/state.py b/discord/state.py index b3da4eabf7e6..fbde85cf0fef 100644 --- a/discord/state.py +++ b/discord/state.py @@ -831,6 +831,12 @@ def parse_channel_delete(self, data: gw.ChannelDeleteEvent) -> None: guild._scheduled_events.pop(s.id) self.dispatch('scheduled_event_delete', s) + threads = guild._remove_threads_by_channel(channel_id) + + for thread in threads: + self.dispatch('thread_delete', thread) + self.dispatch('raw_thread_delete', RawThreadDeleteEvent._from_thread(thread)) + def parse_channel_update(self, data: gw.ChannelUpdateEvent) -> None: channel_type = try_enum(ChannelType, data.get('type')) channel_id = int(data['id']) From 28924019922d180b15a23218df6a1f50e531acd4 Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Sun, 5 May 2024 05:24:34 +0200 Subject: [PATCH 033/354] Add support for one-time purchases --- discord/enums.py | 9 +++++++++ discord/http.py | 10 ++++++++++ discord/sku.py | 24 ++++++++++++++++++++++++ discord/types/sku.py | 3 ++- docs/api.rst | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/discord/enums.py b/discord/enums.py index 0c6f93fdf2ec..f1af2d790500 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -796,11 +796,20 @@ class SelectDefaultValueType(Enum): class SKUType(Enum): + durable = 2 + consumable = 3 subscription = 5 subscription_group = 6 class EntitlementType(Enum): + purchase = 1 + premium_subscription = 2 + developer_gift = 3 + test_mode_purchase = 4 + free_purchase = 5 + user_gift = 6 + premium_purchase = 7 application_subscription = 8 diff --git a/discord/http.py b/discord/http.py index 0979942a980e..f36d191e4b0e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2454,6 +2454,16 @@ def get_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) ), ) + def consume_entitlement(self, application_id: Snowflake, entitlement_id: Snowflake) -> Response[None]: + return self.request( + Route( + 'POST', + '/applications/{application_id}/entitlements/{entitlement_id}/consume', + application_id=application_id, + entitlement_id=entitlement_id, + ), + ) + def create_entitlement( self, application_id: Snowflake, sku_id: Snowflake, owner_id: Snowflake, owner_type: sku.EntitlementOwnerType ) -> Response[sku.Entitlement]: diff --git a/discord/sku.py b/discord/sku.py index 0125c5734c3b..2af171c1d97a 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -126,6 +126,8 @@ class Entitlement: A UTC date which entitlement is no longer valid. Not present when using test entitlements. guild_id: Optional[:class:`int`] The ID of the guild that is granted access to the entitlement + consumed: :class:`bool` + For consumable items, whether the entitlement has been consumed. """ __slots__ = ( @@ -139,6 +141,7 @@ class Entitlement: 'starts_at', 'ends_at', 'guild_id', + 'consumed', ) def __init__(self, state: ConnectionState, data: EntitlementPayload): @@ -152,6 +155,7 @@ def __init__(self, state: ConnectionState, data: EntitlementPayload): self.starts_at: Optional[datetime] = utils.parse_time(data.get('starts_at', None)) self.ends_at: Optional[datetime] = utils.parse_time(data.get('ends_at', None)) self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') + self.consumed: bool = data.get('consumed', False) def __repr__(self) -> str: return f'' @@ -179,6 +183,26 @@ def is_expired(self) -> bool: return False return utils.utcnow() >= self.ends_at + async def consume(self) -> None: + """|coro| + + Marks a one-time purchase entitlement as consumed. + + Raises + ------- + MissingApplicationID + The application ID could not be found. + NotFound + The entitlement could not be found. + HTTPException + Consuming the entitlement failed. + """ + + if self.application_id is None: + raise MissingApplicationID + + await self._state.http.consume_entitlement(self.application_id, self.id) + async def delete(self) -> None: """|coro| diff --git a/discord/types/sku.py b/discord/types/sku.py index 9ff3cfb13331..a49e0d6596f0 100644 --- a/discord/types/sku.py +++ b/discord/types/sku.py @@ -46,7 +46,8 @@ class Entitlement(TypedDict): deleted: bool starts_at: NotRequired[str] ends_at: NotRequired[str] - guild_id: Optional[str] + guild_id: NotRequired[str] + consumed: NotRequired[bool] EntitlementOwnerType = Literal[1, 2] diff --git a/docs/api.rst b/docs/api.rst index 11e5a2af18ba..b4285c3c1967 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3506,6 +3506,14 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: durable + + The SKU is a durable one-time purchase. + + .. attribute:: consumable + + The SKU is a consumable one-time purchase. + .. attribute:: subscription The SKU is a recurring subscription. @@ -3521,6 +3529,34 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: purchase + + The entitlement was purchased by the user. + + .. attribute:: premium_subscription + + The entitlement is for a nitro subscription. + + .. attribute:: developer_gift + + The entitlement was gifted by the developer. + + .. attribute:: test_mode_purchase + + The entitlement was purchased by a developer in application test mode. + + .. attribute:: free_purchase + + The entitlement was granted, when the SKU was free. + + .. attribute:: user_gift + + The entitlement was gifted by a another user. + + .. attribute:: premium_purchase + + The entitlement was claimed for free by a nitro subscriber. + .. attribute:: application_subscription The entitlement was purchased as an app subscription. From 2e2f51fd5c95b3c40fd1c67ae4b08bbeeaeaddce Mon Sep 17 00:00:00 2001 From: Danny <1695103+Rapptz@users.noreply.github.com> Date: Sat, 4 May 2024 23:25:01 -0400 Subject: [PATCH 034/354] First pass at supporting user apps Co-authored-by: red Co-authored-by: Vioshim <63890837+Vioshim@users.noreply.github.com> --- discord/app_commands/__init__.py | 1 + discord/app_commands/commands.py | 356 +++++++++++++++++++++++++++++- discord/app_commands/installs.py | 207 +++++++++++++++++ discord/app_commands/models.py | 35 ++- discord/app_commands/namespace.py | 4 +- discord/app_commands/tree.py | 28 ++- discord/channel.py | 21 +- discord/ext/commands/bot.py | 22 +- discord/ext/commands/cog.py | 2 + discord/ext/commands/context.py | 4 +- discord/ext/commands/hybrid.py | 4 + discord/flags.py | 176 +++++++++++++-- discord/guild.py | 7 +- discord/interactions.py | 37 +++- discord/member.py | 11 +- discord/state.py | 7 +- discord/types/command.py | 4 + discord/types/interactions.py | 12 + docs/interactions/api.rst | 34 +++ 19 files changed, 920 insertions(+), 52 deletions(-) create mode 100644 discord/app_commands/installs.py diff --git a/discord/app_commands/__init__.py b/discord/app_commands/__init__.py index 971461713449..a338cab75dc5 100644 --- a/discord/app_commands/__init__.py +++ b/discord/app_commands/__init__.py @@ -16,5 +16,6 @@ from .namespace import * from .transformers import * from .translator import * +from .installs import * from . import checks as checks from .checks import Cooldown as Cooldown diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 6f46fbe4c978..23fe953a12a5 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -49,6 +49,7 @@ from copy import copy as shallow_copy from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale +from .installs import AppCommandContext, AppInstallationType from .models import Choice from .transformers import annotation_to_parameter, CommandParameter, NoneType from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered @@ -65,6 +66,8 @@ from ..abc import Snowflake from .namespace import Namespace from .models import ChoiceT + from .tree import CommandTree + from .._types import ClientT # Generally, these two libraries are supposed to be separate from each other. # However, for type hinting purposes it's unfortunately necessary for one to @@ -87,6 +90,12 @@ 'autocomplete', 'guilds', 'guild_only', + 'dm_only', + 'private_channel_only', + 'allowed_contexts', + 'guild_install', + 'user_install', + 'allowed_installs', 'default_permissions', ) @@ -618,6 +627,16 @@ class Command(Generic[GroupT, P, T]): Whether the command should only be usable in guild contexts. Due to a Discord limitation, this does not work on subcommands. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that the command is allowed to be used in. + Overrides ``guild_only`` if this is set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. @@ -638,6 +657,8 @@ def __init__( nsfw: bool = False, parent: Optional[Group] = None, guild_ids: Optional[List[int]] = None, + allowed_contexts: Optional[AppCommandContext] = None, + allowed_installs: Optional[AppInstallationType] = None, auto_locale_strings: bool = True, extras: Dict[Any, Any] = MISSING, ): @@ -672,6 +693,13 @@ def __init__( callback, '__discord_app_commands_default_permissions__', None ) self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False) + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr( + callback, '__discord_app_commands_contexts__', None + ) + self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr( + callback, '__discord_app_commands_installation_types__', None + ) + self.nsfw: bool = nsfw self.extras: Dict[Any, Any] = extras or {} @@ -718,8 +746,8 @@ def _copy_with( return copy - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} @@ -745,7 +773,7 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] ] return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: # If we have a parent then our type is a subcommand # Otherwise, the type falls back to the specific command type (e.g. slash command or context menu) option_type = AppCommandType.chat_input.value if self.parent is None else AppCommandOptionType.subcommand.value @@ -760,6 +788,8 @@ def to_dict(self) -> Dict[str, Any]: base['nsfw'] = self.nsfw base['dm_permission'] = not self.guild_only base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value + base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts) + base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs) return base @@ -1167,6 +1197,16 @@ class ContextMenu: guild_only: :class:`bool` Whether the command should only be usable in guild contexts. Defaults to ``False``. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this context menu is allowed to be used in. + Overrides ``guild_only`` if set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. Defaults to ``False``. @@ -1189,6 +1229,8 @@ def __init__( type: AppCommandType = MISSING, nsfw: bool = False, guild_ids: Optional[List[int]] = None, + allowed_contexts: Optional[AppCommandContext] = None, + allowed_installs: Optional[AppInstallationType] = None, auto_locale_strings: bool = True, extras: Dict[Any, Any] = MISSING, ): @@ -1214,6 +1256,12 @@ def __init__( ) self.nsfw: bool = nsfw self.guild_only: bool = getattr(callback, '__discord_app_commands_guild_only__', False) + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts or getattr( + callback, '__discord_app_commands_contexts__', None + ) + self.allowed_installs: Optional[AppInstallationType] = allowed_installs or getattr( + callback, '__discord_app_commands_installation_types__', None + ) self.checks: List[Check] = getattr(callback, '__discord_app_commands_checks__', []) self.extras: Dict[Any, Any] = extras or {} @@ -1231,8 +1279,8 @@ def qualified_name(self) -> str: """:class:`str`: Returns the fully qualified command name.""" return self.name - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) context = TranslationContext(location=TranslationContextLocation.command_name, data=self) if self._locale_name: name_localizations: Dict[str, str] = {} @@ -1244,11 +1292,13 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] base['name_localizations'] = name_localizations return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: return { 'name': self.name, 'type': self.type.value, 'dm_permission': not self.guild_only, + 'contexts': tree.allowed_contexts._merge_to_array(self.allowed_contexts), + 'integration_types': tree.allowed_installs._merge_to_array(self.allowed_installs), 'default_member_permissions': None if self.default_permissions is None else self.default_permissions.value, 'nsfw': self.nsfw, } @@ -1405,6 +1455,16 @@ class shortened to 100 characters. Whether the group should only be usable in guild contexts. Due to a Discord limitation, this does not work on subcommands. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this group is allowed to be used in. Overrides + guild_only if set. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that the command is allowed to be installed + on. + + .. versionadded:: 2.4 nsfw: :class:`bool` Whether the command is NSFW and should only work in NSFW channels. @@ -1424,6 +1484,8 @@ class shortened to 100 characters. __discord_app_commands_group_locale_description__: Optional[locale_str] = None __discord_app_commands_group_nsfw__: bool = False __discord_app_commands_guild_only__: bool = MISSING + __discord_app_commands_contexts__: Optional[AppCommandContext] = MISSING + __discord_app_commands_installation_types__: Optional[AppInstallationType] = MISSING __discord_app_commands_default_permissions__: Optional[Permissions] = MISSING __discord_app_commands_has_module__: bool = False __discord_app_commands_error_handler__: Optional[ @@ -1492,6 +1554,8 @@ def __init__( parent: Optional[Group] = None, guild_ids: Optional[List[int]] = None, guild_only: bool = MISSING, + allowed_contexts: Optional[AppCommandContext] = MISSING, + allowed_installs: Optional[AppInstallationType] = MISSING, nsfw: bool = MISSING, auto_locale_strings: bool = True, default_permissions: Optional[Permissions] = MISSING, @@ -1540,6 +1604,22 @@ def __init__( self.guild_only: bool = guild_only + if allowed_contexts is MISSING: + if cls.__discord_app_commands_contexts__ is MISSING: + allowed_contexts = None + else: + allowed_contexts = cls.__discord_app_commands_contexts__ + + self.allowed_contexts: Optional[AppCommandContext] = allowed_contexts + + if allowed_installs is MISSING: + if cls.__discord_app_commands_installation_types__ is MISSING: + allowed_installs = None + else: + allowed_installs = cls.__discord_app_commands_installation_types__ + + self.allowed_installs: Optional[AppInstallationType] = allowed_installs + if nsfw is MISSING: nsfw = cls.__discord_app_commands_group_nsfw__ @@ -1633,8 +1713,8 @@ def _copy_with( return copy - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: - base = self.to_dict() + async def get_translated_payload(self, tree: CommandTree[ClientT], translator: Translator) -> Dict[str, Any]: + base = self.to_dict(tree) name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} @@ -1654,10 +1734,10 @@ async def get_translated_payload(self, translator: Translator) -> Dict[str, Any] base['name_localizations'] = name_localizations base['description_localizations'] = description_localizations - base['options'] = [await child.get_translated_payload(translator) for child in self._children.values()] + base['options'] = [await child.get_translated_payload(tree, translator) for child in self._children.values()] return base - def to_dict(self) -> Dict[str, Any]: + def to_dict(self, tree: CommandTree[ClientT]) -> Dict[str, Any]: # If this has a parent command then it's part of a subcommand group # Otherwise, it's just a regular command option_type = 1 if self.parent is None else AppCommandOptionType.subcommand_group.value @@ -1665,13 +1745,15 @@ def to_dict(self) -> Dict[str, Any]: 'name': self.name, 'description': self.description, 'type': option_type, - 'options': [child.to_dict() for child in self._children.values()], + 'options': [child.to_dict(tree) for child in self._children.values()], } if self.parent is None: base['nsfw'] = self.nsfw base['dm_permission'] = not self.guild_only base['default_member_permissions'] = None if self.default_permissions is None else self.default_permissions.value + base['contexts'] = tree.allowed_contexts._merge_to_array(self.allowed_contexts) + base['integration_types'] = tree.allowed_installs._merge_to_array(self.allowed_installs) return base @@ -2421,8 +2503,181 @@ async def my_guild_only_command(interaction: discord.Interaction) -> None: def inner(f: T) -> T: if isinstance(f, (Command, Group, ContextMenu)): f.guild_only = True + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts else: f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment + + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.guild = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command can only be used in the context of DMs and group DMs. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + Therefore, there is no error handler called when a command is used within a guild. + + This decorator can be called with or without parentheses. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.private_channel_only() + async def my_private_channel_only_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in DMs and GDMs!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.private_channel = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command can only be used in the context of bot DMs. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + Therefore, there is no error handler called when a command is used within a guild or group DM. + + This decorator can be called with or without parentheses. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.dm_only() + async def my_dm_only_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in DMs!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + allowed_contexts.dm_channel = True + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +def allowed_contexts( + guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING +) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command can only be used in certain contexts. + Valid contexts are guilds, DMs and private channels. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.allowed_contexts(guilds=True, dms=False, private_channels=True) + async def my_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am only available in guilds and private channels!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + f.guild_only = False + allowed_contexts = f.allowed_contexts or AppCommandContext() + f.allowed_contexts = allowed_contexts + else: + allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() + f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment + + if guilds is not MISSING: + allowed_contexts.guild = guilds + + if dms is not MISSING: + allowed_contexts.dm_channel = dms + + if private_channels is not MISSING: + allowed_contexts.private_channel = private_channels + + return f + + return inner + + +def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command should be installed in guilds. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.guild_install() + async def my_guild_install_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in guilds by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + allowed_installs.guild = True + return f # Check if called with parentheses or not @@ -2433,6 +2688,85 @@ def inner(f: T) -> T: return inner(func) +def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command should be installed for users. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.user_install() + async def my_user_install_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in users by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + allowed_installs.user = True + + return f + + # Check if called with parentheses or not + if func is None: + # Called with parentheses + return inner + else: + return inner(func) + + +def allowed_installs( + guilds: bool = MISSING, + users: bool = MISSING, +) -> Union[T, Callable[[T], T]]: + """A decorator that indicates this command should be installed in certain contexts. + Valid contexts are guilds and users. + + This is **not** implemented as a :func:`check`, and is instead verified by Discord server side. + + Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + + Examples + --------- + + .. code-block:: python3 + + @app_commands.command() + @app_commands.allowed_installs(guilds=False, users=True) + async def my_command(interaction: discord.Interaction) -> None: + await interaction.response.send_message('I am installed in users by default!') + """ + + def inner(f: T) -> T: + if isinstance(f, (Command, Group, ContextMenu)): + allowed_installs = f.allowed_installs or AppInstallationType() + f.allowed_installs = allowed_installs + else: + allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() + f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment + + if guilds is not MISSING: + allowed_installs.guild = guilds + + if users is not MISSING: + allowed_installs.user = users + + return f + + return inner + + def default_permissions(**perms: bool) -> Callable[[T], T]: r"""A decorator that sets the default permissions needed to execute this command. diff --git a/discord/app_commands/installs.py b/discord/app_commands/installs.py new file mode 100644 index 000000000000..7d9b2f049245 --- /dev/null +++ b/discord/app_commands/installs.py @@ -0,0 +1,207 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence + +__all__ = ( + 'AppInstallationType', + 'AppCommandContext', +) + +if TYPE_CHECKING: + from typing_extensions import Self + from ..types.interactions import InteractionContextType, InteractionInstallationType + + +class AppInstallationType: + r"""Represents the installation location of an application command. + + .. versionadded:: 2.4 + + Parameters + ----------- + guild: Optional[:class:`bool`] + Whether the integration is a guild install. + user: Optional[:class:`bool`] + Whether the integration is a user install. + """ + + __slots__ = ('_guild', '_user') + + GUILD: ClassVar[int] = 0 + USER: ClassVar[int] = 1 + + def __init__(self, *, guild: Optional[bool] = None, user: Optional[bool] = None): + self._guild: Optional[bool] = guild + self._user: Optional[bool] = user + + @property + def guild(self) -> bool: + """:class:`bool`: Whether the integration is a guild install.""" + return bool(self._guild) + + @guild.setter + def guild(self, value: bool) -> None: + self._guild = bool(value) + + @property + def user(self) -> bool: + """:class:`bool`: Whether the integration is a user install.""" + return bool(self._user) + + @user.setter + def user(self, value: bool) -> None: + self._user = bool(value) + + def merge(self, other: AppInstallationType) -> AppInstallationType: + # Merging is similar to AllowedMentions where `self` is the base + # and the `other` is the override preference + guild = self.guild if other.guild is None else other.guild + user = self.user if other.user is None else other.user + return AppInstallationType(guild=guild, user=user) + + def _is_unset(self) -> bool: + return all(x is None for x in (self._guild, self._user)) + + def _merge_to_array(self, other: Optional[AppInstallationType]) -> Optional[List[InteractionInstallationType]]: + result = self.merge(other) if other is not None else self + if result._is_unset(): + return None + return result.to_array() + + @classmethod + def _from_value(cls, value: Sequence[InteractionInstallationType]) -> Self: + self = cls() + for x in value: + if x == cls.GUILD: + self._guild = True + elif x == cls.USER: + self._user = True + return self + + def to_array(self) -> List[InteractionInstallationType]: + values = [] + if self._guild: + values.append(self.GUILD) + if self._user: + values.append(self.USER) + return values + + +class AppCommandContext: + r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context. + + .. versionadded:: 2.4 + + Parameters + ----------- + guild: Optional[:class:`bool`] + Whether the context allows usage in a guild. + dm_channel: Optional[:class:`bool`] + Whether the context allows usage in a DM channel. + private_channel: Optional[:class:`bool`] + Whether the context allows usage in a DM or a GDM channel. + """ + + GUILD: ClassVar[int] = 0 + DM_CHANNEL: ClassVar[int] = 1 + PRIVATE_CHANNEL: ClassVar[int] = 2 + + __slots__ = ('_guild', '_dm_channel', '_private_channel') + + def __init__( + self, + *, + guild: Optional[bool] = None, + dm_channel: Optional[bool] = None, + private_channel: Optional[bool] = None, + ): + self._guild: Optional[bool] = guild + self._dm_channel: Optional[bool] = dm_channel + self._private_channel: Optional[bool] = private_channel + + @property + def guild(self) -> bool: + """:class:`bool`: Whether the context allows usage in a guild.""" + return bool(self._guild) + + @guild.setter + def guild(self, value: bool) -> None: + self._guild = bool(value) + + @property + def dm_channel(self) -> bool: + """:class:`bool`: Whether the context allows usage in a DM channel.""" + return bool(self._dm_channel) + + @dm_channel.setter + def dm_channel(self, value: bool) -> None: + self._dm_channel = bool(value) + + @property + def private_channel(self) -> bool: + """:class:`bool`: Whether the context allows usage in a DM or a GDM channel.""" + return bool(self._private_channel) + + @private_channel.setter + def private_channel(self, value: bool) -> None: + self._private_channel = bool(value) + + def merge(self, other: AppCommandContext) -> AppCommandContext: + guild = self.guild if other.guild is None else other.guild + dm_channel = self.dm_channel if other.dm_channel is None else other.dm_channel + private_channel = self.private_channel if other.private_channel is None else other.private_channel + return AppCommandContext(guild=guild, dm_channel=dm_channel, private_channel=private_channel) + + def _is_unset(self) -> bool: + return all(x is None for x in (self._guild, self._dm_channel, self._private_channel)) + + def _merge_to_array(self, other: Optional[AppCommandContext]) -> Optional[List[InteractionContextType]]: + result = self.merge(other) if other is not None else self + if result._is_unset(): + return None + return result.to_array() + + @classmethod + def _from_value(cls, value: Sequence[InteractionContextType]) -> Self: + self = cls() + for x in value: + if x == cls.GUILD: + self._guild = True + elif x == cls.DM_CHANNEL: + self._dm_channel = True + elif x == cls.PRIVATE_CHANNEL: + self._private_channel = True + return self + + def to_array(self) -> List[InteractionContextType]: + values = [] + if self._guild: + values.append(self.GUILD) + if self._dm_channel: + values.append(self.DM_CHANNEL) + if self._private_channel: + values.append(self.PRIVATE_CHANNEL) + return values diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index 3e9d250b283b..e8a96784b87c 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -26,9 +26,17 @@ from datetime import datetime from .errors import MissingApplicationID +from ..flags import AppCommandContext, AppInstallationType from .translator import TranslationContextLocation, TranslationContext, locale_str, Translator from ..permissions import Permissions -from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum +from ..enums import ( + AppCommandOptionType, + AppCommandType, + AppCommandPermissionType, + ChannelType, + Locale, + try_enum, +) from ..mixins import Hashable from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING from ..object import Object @@ -160,6 +168,14 @@ class AppCommand(Hashable): The default member permissions that can run this command. dm_permission: :class:`bool` A boolean that indicates whether this command can be run in direct messages. + allowed_contexts: Optional[:class:`~discord.app_commands.AppCommandContext`] + The contexts that this command is allowed to be used in. Overrides the ``dm_permission`` attribute. + + .. versionadded:: 2.4 + allowed_installs: Optional[:class:`~discord.app_commands.AppInstallationType`] + The installation contexts that this command is allowed to be installed in. + + .. versionadded:: 2.4 guild_id: Optional[:class:`int`] The ID of the guild this command is registered in. A value of ``None`` denotes that it is a global command. @@ -179,6 +195,8 @@ class AppCommand(Hashable): 'options', 'default_member_permissions', 'dm_permission', + 'allowed_contexts', + 'allowed_installs', 'nsfw', '_state', ) @@ -210,6 +228,19 @@ def _from_data(self, data: ApplicationCommandPayload) -> None: dm_permission = True self.dm_permission: bool = dm_permission + + allowed_contexts = data.get('contexts') + if allowed_contexts is None: + self.allowed_contexts: Optional[AppCommandContext] = None + else: + self.allowed_contexts = AppCommandContext._from_value(allowed_contexts) + + allowed_installs = data.get('integration_types') + if allowed_installs is None: + self.allowed_installs: Optional[AppInstallationType] = None + else: + self.allowed_installs = AppInstallationType._from_value(allowed_installs) + self.nsfw: bool = data.get('nsfw', False) self.name_localizations: Dict[Locale, str] = _to_locale_dict(data.get('name_localizations') or {}) self.description_localizations: Dict[Locale, str] = _to_locale_dict(data.get('description_localizations') or {}) @@ -223,6 +254,8 @@ def to_dict(self) -> ApplicationCommandPayload: 'description': self.description, 'name_localizations': {str(k): v for k, v in self.name_localizations.items()}, 'description_localizations': {str(k): v for k, v in self.description_localizations.items()}, + 'contexts': self.allowed_contexts.to_array() if self.allowed_contexts is not None else None, + 'integration_types': self.allowed_installs.to_array() if self.allowed_installs is not None else None, 'options': [opt.to_dict() for opt in self.options], } # type: ignore # Type checker does not understand this literal. diff --git a/discord/app_commands/namespace.py b/discord/app_commands/namespace.py index 7fad617c679c..3fa81712cff3 100644 --- a/discord/app_commands/namespace.py +++ b/discord/app_commands/namespace.py @@ -179,7 +179,7 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - state = interaction._state members = resolved.get('members', {}) guild_id = interaction.guild_id - guild = state._get_or_create_unavailable_guild(guild_id) if guild_id is not None else None + guild = interaction.guild type = AppCommandOptionType.user.value for (user_id, user_data) in resolved.get('users', {}).items(): try: @@ -220,7 +220,6 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - } ) - guild = state._get_guild(guild_id) for (message_id, message_data) in resolved.get('messages', {}).items(): channel_id = int(message_data['channel_id']) if guild is None: @@ -232,6 +231,7 @@ def _get_resolved_items(cls, interaction: Interaction, resolved: ResolvedData) - # Type checker doesn't understand this due to failure to narrow message = Message(state=state, channel=channel, data=message_data) # type: ignore + message.guild = guild key = ResolveKey(id=message_id, type=-1) completed[key] = message diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index c75682e0ea2c..abd8924806fd 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -58,6 +58,7 @@ CommandSyncFailure, MissingApplicationID, ) +from .installs import AppCommandContext, AppInstallationType from .translator import Translator, locale_str from ..errors import ClientException, HTTPException from ..enums import AppCommandType, InteractionType @@ -121,9 +122,26 @@ class CommandTree(Generic[ClientT]): to find the guild-specific ``/ping`` command it will fall back to the global ``/ping`` command. This has the potential to raise more :exc:`~discord.app_commands.CommandSignatureMismatch` errors than usual. Defaults to ``True``. + allowed_contexts: :class:`~discord.app_commands.AppCommandContext` + The default allowed contexts that applies to all commands in this tree. + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 + allowed_installs: :class:`~discord.app_commands.AppInstallationType` + The default allowed install locations that apply to all commands in this tree. + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 """ - def __init__(self, client: ClientT, *, fallback_to_global: bool = True): + def __init__( + self, + client: ClientT, + *, + fallback_to_global: bool = True, + allowed_contexts: AppCommandContext = MISSING, + allowed_installs: AppInstallationType = MISSING, + ): self.client: ClientT = client self._http = client.http self._state = client._connection @@ -133,6 +151,8 @@ def __init__(self, client: ClientT, *, fallback_to_global: bool = True): self._state._command_tree = self self.fallback_to_global: bool = fallback_to_global + self.allowed_contexts = AppCommandContext() if allowed_contexts is MISSING else allowed_contexts + self.allowed_installs = AppInstallationType() if allowed_installs is MISSING else allowed_installs self._guild_commands: Dict[int, Dict[str, Union[Command, Group]]] = {} self._global_commands: Dict[str, Union[Command, Group]] = {} # (name, guild_id, command_type): Command @@ -722,7 +742,7 @@ def walk_commands( else: guild_id = None if guild is None else guild.id value = type.value - for ((_, g, t), command) in self._context_menus.items(): + for (_, g, t), command in self._context_menus.items(): if g == guild_id and t == value: yield command @@ -1058,9 +1078,9 @@ async def sync(self, *, guild: Optional[Snowflake] = None) -> List[AppCommand]: translator = self.translator if translator: - payload = [await command.get_translated_payload(translator) for command in commands] + payload = [await command.get_translated_payload(self, translator) for command in commands] else: - payload = [command.to_dict() for command in commands] + payload = [command.to_dict(self) for command in commands] try: if guild is None: diff --git a/discord/channel.py b/discord/channel.py index 52bb4706903b..f60e22c0d91a 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2916,22 +2916,21 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable): The user you are participating with in the direct message channel. If this channel is received through the gateway, the recipient information may not be always available. + recipients: List[:class:`User`] + The users you are participating with in the DM channel. + + .. versionadded:: 2.4 me: :class:`ClientUser` The user presenting yourself. id: :class:`int` The direct message channel ID. """ - __slots__ = ('id', 'recipient', 'me', '_state') + __slots__ = ('id', 'recipients', 'me', '_state') def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): self._state: ConnectionState = state - self.recipient: Optional[User] = None - - recipients = data.get('recipients') - if recipients is not None: - self.recipient = state.store_user(recipients[0]) - + self.recipients: List[User] = [state.store_user(u) for u in data.get('recipients', [])] self.me: ClientUser = me self.id: int = int(data['id']) @@ -2951,11 +2950,17 @@ def _from_message(cls, state: ConnectionState, channel_id: int) -> Self: self = cls.__new__(cls) self._state = state self.id = channel_id - self.recipient = None + self.recipients = [] # state.user won't be None here self.me = state.user # type: ignore return self + @property + def recipient(self) -> Optional[User]: + if self.recipients: + return self.recipients[0] + return None + @property def type(self) -> Literal[ChannelType.private]: """:class:`ChannelType`: The channel's Discord type.""" diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index b691c5af29a9..208948335568 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -166,6 +166,8 @@ def __init__( help_command: Optional[HelpCommand] = _default, tree_cls: Type[app_commands.CommandTree[Any]] = app_commands.CommandTree, description: Optional[str] = None, + allowed_contexts: app_commands.AppCommandContext = MISSING, + allowed_installs: app_commands.AppInstallationType = MISSING, intents: discord.Intents, **options: Any, ) -> None: @@ -174,6 +176,11 @@ def __init__( self.extra_events: Dict[str, List[CoroFunc]] = {} # Self doesn't have the ClientT bound, but since this is a mixin it technically does self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore + if allowed_contexts is not MISSING: + self.__tree.allowed_contexts = allowed_contexts + if allowed_installs is not MISSING: + self.__tree.allowed_installs = allowed_installs + self.__cogs: Dict[str, Cog] = {} self.__extensions: Dict[str, types.ModuleType] = {} self._checks: List[UserCheck] = [] @@ -521,7 +528,6 @@ async def is_owner(self, user: User, /) -> bool: elif self.owner_ids: return user.id in self.owner_ids else: - app: discord.AppInfo = await self.application_info() # type: ignore if app.team: self.owner_ids = ids = { @@ -1489,6 +1495,20 @@ class Bot(BotBase, discord.Client): The type of application command tree to use. Defaults to :class:`~discord.app_commands.CommandTree`. .. versionadded:: 2.0 + allowed_contexts: :class:`~discord.app_commands.AppCommandContext` + The default allowed contexts that applies to all application commands + in the application command tree. + + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 + allowed_installs: :class:`~discord.app_commands.AppInstallationType` + The default allowed install locations that apply to all application commands + in the application command tree. + + Note that you can override this on a per command basis. + + .. versionadded:: 2.4 """ pass diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 54842c2599a9..659d69ebb433 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -318,6 +318,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: parent=None, guild_ids=getattr(cls, '__discord_app_commands_default_guilds__', None), guild_only=getattr(cls, '__discord_app_commands_guild_only__', False), + allowed_contexts=getattr(cls, '__discord_app_commands_contexts__', None), + allowed_installs=getattr(cls, '__discord_app_commands_installation_types__', None), default_permissions=getattr(cls, '__discord_app_commands_default_permissions__', None), extras=cls.__cog_group_extras__, ) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 5fc675acdb34..d4052cbbd914 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -472,7 +472,7 @@ def permissions(self) -> Permissions: .. versionadded:: 2.0 """ - if self.channel.type is ChannelType.private: + if self.interaction is None and self.channel.type is ChannelType.private: return Permissions._dm_permissions() if not self.interaction: # channel and author will always match relevant types here @@ -506,7 +506,7 @@ def bot_permissions(self) -> Permissions: .. versionadded:: 2.0 """ channel = self.channel - if channel.type == ChannelType.private: + if self.interaction is None and channel.type == ChannelType.private: return Permissions._dm_permissions() if not self.interaction: # channel and me will always match relevant types here diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index c9797e734eeb..8c2f9a9e9d65 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -653,6 +653,8 @@ def __init__( guild_only = getattr(self.callback, '__discord_app_commands_guild_only__', False) default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None) nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__', False) + contexts = getattr(self.callback, '__discord_app_commands_contexts__', MISSING) + installs = getattr(self.callback, '__discord_app_commands_installation_types__', MISSING) self.app_command = app_commands.Group( name=self._locale_name or self.name, description=self._locale_description or self.description or self.short_doc or '…', @@ -660,6 +662,8 @@ def __init__( guild_only=guild_only, default_permissions=default_permissions, nsfw=nsfw, + allowed_installs=installs, + allowed_contexts=contexts, ) # This prevents the group from re-adding the command at __init__ diff --git a/discord/flags.py b/discord/flags.py index 6e5721fcf39a..249c2e8f6c12 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -58,8 +58,10 @@ 'ChannelFlags', 'AutoModPresets', 'MemberFlags', + 'AppCommandContext', 'AttachmentFlags', 'RoleFlags', + 'AppInstallationType', 'SKUFlags', ) @@ -1660,8 +1662,24 @@ def _from_value(cls: Type[Self], value: Sequence[int]) -> Self: self.value = reduce(or_, map((1).__lshift__, value), 0) >> 1 return self - def to_array(self) -> List[int]: - return [i + 1 for i in range(self.value.bit_length()) if self.value & (1 << i)] + def to_array(self, *, offset: int = 0) -> List[int]: + return [i + offset for i in range(self.value.bit_length()) if self.value & (1 << i)] + + @classmethod + def all(cls: Type[Self]) -> Self: + """A factory method that creates an instance of ArrayFlags with everything enabled.""" + bits = max(cls.VALID_FLAGS.values()).bit_length() + value = (1 << bits) - 1 + self = cls.__new__(cls) + self.value = value + return self + + @classmethod + def none(cls: Type[Self]) -> Self: + """A factory method that creates an instance of ArrayFlags with everything disabled.""" + self = cls.__new__(cls) + self.value = self.DEFAULT_VALUE + return self @fill_with_flags() @@ -1728,6 +1746,9 @@ class AutoModPresets(ArrayFlags): rather than using this raw value. """ + def to_array(self) -> List[int]: + return super().to_array(offset=1) + @flag_value def profanity(self): """:class:`bool`: Whether to use the preset profanity filter.""" @@ -1743,21 +1764,144 @@ def slurs(self): """:class:`bool`: Whether to use the preset slurs filter.""" return 1 << 2 - @classmethod - def all(cls: Type[Self]) -> Self: - """A factory method that creates a :class:`AutoModPresets` with everything enabled.""" - bits = max(cls.VALID_FLAGS.values()).bit_length() - value = (1 << bits) - 1 - self = cls.__new__(cls) - self.value = value - return self - @classmethod - def none(cls: Type[Self]) -> Self: - """A factory method that creates a :class:`AutoModPresets` with everything disabled.""" - self = cls.__new__(cls) - self.value = self.DEFAULT_VALUE - return self +@fill_with_flags() +class AppCommandContext(ArrayFlags): + r"""Wraps up the Discord :class:`~discord.app_commands.Command` execution context. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two AppCommandContext flags are equal. + + .. describe:: x != y + + Checks if two AppCommandContext flags are not equal. + + .. describe:: x | y, x |= y + + Returns an AppCommandContext instance with all enabled flags from + both x and y. + + .. describe:: x & y, x &= y + + Returns an AppCommandContext instance with only flags enabled on + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an AppCommandContext instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an AppCommandContext instance with all flags inverted from x + + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + DEFAULT_VALUE = 3 + + @flag_value + def guild(self): + """:class:`bool`: Whether the context allows usage in a guild.""" + return 1 << 0 + + @flag_value + def dm_channel(self): + """:class:`bool`: Whether the context allows usage in a DM channel.""" + return 1 << 1 + + @flag_value + def private_channel(self): + """:class:`bool`: Whether the context allows usage in a DM or a GDM channel.""" + return 1 << 2 + + +@fill_with_flags() +class AppInstallationType(ArrayFlags): + r"""Represents the installation location of an application command. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two AppInstallationType flags are equal. + + .. describe:: x != y + + Checks if two AppInstallationType flags are not equal. + + .. describe:: x | y, x |= y + + Returns an AppInstallationType instance with all enabled flags from + both x and y. + + .. describe:: x & y, x &= y + + Returns an AppInstallationType instance with only flags enabled on + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an AppInstallationType instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an AppInstallationType instance with all flags inverted from x + + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @flag_value + def guild(self): + """:class:`bool`: Whether the integration is a guild install.""" + return 1 << 0 + + @flag_value + def user(self): + """:class:`bool`: Whether the integration is a user install.""" + return 1 << 1 @fill_with_flags() diff --git a/discord/guild.py b/discord/guild.py index 6d1c5bde31af..2a23d193fce1 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -455,8 +455,11 @@ def _remove_role(self, role_id: int, /) -> Role: return role @classmethod - def _create_unavailable(cls, *, state: ConnectionState, guild_id: int) -> Guild: - return cls(state=state, data={'id': guild_id, 'unavailable': True}) # type: ignore + def _create_unavailable(cls, *, state: ConnectionState, guild_id: int, data: Optional[Dict[str, Any]]) -> Guild: + if data is None: + data = {'unavailable': True} + data.update(id=guild_id) + return cls(state=state, data=data) # type: ignore def _from_data(self, guild: GuildPayload) -> None: try: diff --git a/discord/interactions.py b/discord/interactions.py index 48e73855e1b0..5638886b348f 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -45,6 +45,7 @@ from .permissions import Permissions from .http import handle_message_parameters from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params +from .app_commands.installs import AppCommandContext from .app_commands.namespace import Namespace from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation from .channel import _threaded_channel_factory @@ -64,6 +65,7 @@ from .types.webhook import ( Webhook as WebhookPayload, ) + from .types.snowflake import Snowflake from .guild import Guild from .state import ConnectionState from .file import File @@ -139,6 +141,10 @@ class Interaction(Generic[ClientT]): command_failed: :class:`bool` Whether the command associated with this interaction failed to execute. This includes checks and execution. + context: :class:`.AppCommandContext` + The context of the interaction. + + .. versionadded:: 2.4 """ __slots__: Tuple[str, ...] = ( @@ -157,6 +163,8 @@ class Interaction(Generic[ClientT]): 'command_failed', 'entitlement_sku_ids', 'entitlements', + "context", + '_integration_owners', '_permissions', '_app_permissions', '_state', @@ -194,6 +202,14 @@ def _from_data(self, data: InteractionPayload): self.application_id: int = int(data['application_id']) self.entitlement_sku_ids: List[int] = [int(x) for x in data.get('entitlement_skus', []) or []] self.entitlements: List[Entitlement] = [Entitlement(self._state, x) for x in data.get('entitlements', [])] + # This is not entirely useful currently, unsure how to expose it in a way that it is. + self._integration_owners: Dict[int, Snowflake] = { + int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items() + } + try: + self.context = AppCommandContext._from_value([data['context']]) + except KeyError: + self.context = AppCommandContext() self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US')) self.guild_locale: Optional[Locale] @@ -204,7 +220,10 @@ def _from_data(self, data: InteractionPayload): guild = None if self.guild_id: - guild = self._state._get_or_create_unavailable_guild(self.guild_id) + # The data type is a TypedDict but it doesn't narrow to Dict[str, Any] properly + guild = self._state._get_or_create_unavailable_guild(self.guild_id, data=data.get('guild')) # type: ignore + if guild.me is None and self._client.user is not None: + guild._add_member(Member._from_client_user(user=self._client.user, guild=guild, state=self._state)) raw_channel = data.get('channel', {}) channel_id = utils._get_as_snowflake(raw_channel, 'id') @@ -371,6 +390,22 @@ def is_expired(self) -> bool: """:class:`bool`: Returns ``True`` if the interaction is expired.""" return utils.utcnow() >= self.expires_at + def is_guild_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a guild integration. + + .. versionadded:: 2.4 + """ + if self.guild_id: + return self.guild_id == self._integration_owners.get(0) + return False + + def is_user_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a user integration. + + .. versionadded:: 2.4 + """ + return self.user.id == self._integration_owners.get(1) + async def original_response(self) -> InteractionMessage: """|coro| diff --git a/discord/member.py b/discord/member.py index 4cfa54a359bd..74ba8693259c 100644 --- a/discord/member.py +++ b/discord/member.py @@ -35,7 +35,7 @@ from . import utils from .asset import Asset from .utils import MISSING -from .user import BaseUser, User, _UserTag +from .user import BaseUser, ClientUser, User, _UserTag from .activity import create_activity, ActivityTypes from .permissions import Permissions from .enums import Status, try_enum @@ -392,6 +392,15 @@ def _from_message(cls, *, message: Message, data: MemberPayload) -> Self: data['user'] = author._to_minimal_user_json() # type: ignore return cls(data=data, guild=message.guild, state=message._state) # type: ignore + @classmethod + def _from_client_user(cls, *, user: ClientUser, guild: Guild, state: ConnectionState) -> Self: + data = { + 'roles': [], + 'user': user._to_minimal_user_json(), + 'flags': 0, + } + return cls(data=data, guild=guild, state=state) # type: ignore + def _update_from_message(self, data: MemberPayload) -> None: self.joined_at = utils.parse_time(data.get('joined_at')) self.premium_since = utils.parse_time(data.get('premium_since')) diff --git a/discord/state.py b/discord/state.py index fbde85cf0fef..a966cb667017 100644 --- a/discord/state.py +++ b/discord/state.py @@ -429,8 +429,8 @@ def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]: # the keys of self._guilds are ints return self._guilds.get(guild_id) # type: ignore - def _get_or_create_unavailable_guild(self, guild_id: int) -> Guild: - return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id) + def _get_or_create_unavailable_guild(self, guild_id: int, *, data: Optional[Dict[str, Any]] = None) -> Guild: + return self._guilds.get(guild_id) or Guild._create_unavailable(state=self, guild_id=guild_id, data=data) def _add_guild(self, guild: Guild) -> None: self._guilds[guild.id] = guild @@ -1592,7 +1592,8 @@ def parse_typing_start(self, data: gw.TypingStartEvent) -> None: if channel is not None: if isinstance(channel, DMChannel): - channel.recipient = raw.user + if raw.user is not None and raw.user not in channel.recipients: + channel.recipients.append(raw.user) elif guild is not None: raw.user = guild.get_member(raw.user_id) diff --git a/discord/types/command.py b/discord/types/command.py index f4eb41ef88eb..7876ee6ddf0e 100644 --- a/discord/types/command.py +++ b/discord/types/command.py @@ -29,9 +29,11 @@ from .channel import ChannelType from .snowflake import Snowflake +from .interactions import InteractionContextType ApplicationCommandType = Literal[1, 2, 3] ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +ApplicationIntegrationType = Literal[0, 1] class _BaseApplicationCommandOption(TypedDict): @@ -141,6 +143,8 @@ class _BaseApplicationCommand(TypedDict): id: Snowflake application_id: Snowflake name: str + contexts: List[InteractionContextType] + integration_types: List[ApplicationIntegrationType] dm_permission: NotRequired[Optional[bool]] default_member_permissions: NotRequired[Optional[str]] nsfw: NotRequired[bool] diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 52bb9c9972f8..d9446ee0eb6f 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -35,12 +35,15 @@ from .role import Role from .snowflake import Snowflake from .user import User +from .guild import GuildFeature if TYPE_CHECKING: from .message import Message InteractionType = Literal[1, 2, 3, 4, 5] +InteractionContextType = Literal[0, 1, 2] +InteractionInstallationType = Literal[0, 1] class _BasePartialChannel(TypedDict): @@ -68,6 +71,12 @@ class ResolvedData(TypedDict, total=False): attachments: Dict[str, Attachment] +class PartialInteractionGuild(TypedDict): + id: Snowflake + locale: str + features: List[GuildFeature] + + class _BaseApplicationCommandInteractionDataOption(TypedDict): name: str @@ -204,6 +213,7 @@ class _BaseInteraction(TypedDict): token: str version: Literal[1] guild_id: NotRequired[Snowflake] + guild: NotRequired[PartialInteractionGuild] channel_id: NotRequired[Snowflake] channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel] app_permissions: NotRequired[str] @@ -211,6 +221,8 @@ class _BaseInteraction(TypedDict): guild_locale: NotRequired[str] entitlement_sku_ids: NotRequired[List[Snowflake]] entitlements: NotRequired[List[Entitlement]] + authorizing_integration_owners: Dict[Literal['0', '1'], Snowflake] + context: NotRequired[InteractionContextType] class PingInteraction(_BaseInteraction): diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 95c1922d181d..6aa234257797 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -129,6 +129,22 @@ AppCommandPermissions .. autoclass:: discord.app_commands.AppCommandPermissions() :members: +AppCommandContext +~~~~~~~~~~~~~~~~~ + +.. attributetable:: discord.app_commands.AppCommandContext + +.. autoclass:: discord.app_commands.AppCommandContext + :members: + +AppInstallationType +~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: discord.app_commands.AppInstallationType + +.. autoclass:: discord.app_commands.AppInstallationType + :members: + GuildAppCommandPermissions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -642,6 +658,24 @@ Decorators .. autofunction:: discord.app_commands.guild_only :decorator: +.. autofunction:: discord.app_commands.dm_only + :decorator: + +.. autofunction:: discord.app_commands.private_channel_only + :decorator: + +.. autofunction:: discord.app_commands.allowed_contexts + :decorator: + +.. autofunction:: discord.app_commands.user_install + :decorator: + +.. autofunction:: discord.app_commands.guild_install + :decorator: + +.. autofunction:: discord.app_commands.allowed_installs + :decorator: + .. autofunction:: discord.app_commands.default_permissions :decorator: From ef64f76eae8a2946eacfac99f0a5af224966952a Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sun, 5 May 2024 08:55:49 +0530 Subject: [PATCH 035/354] Add reason kwarg to Thread.delete --- discord/threads.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/discord/threads.py b/discord/threads.py index b47c189d299f..bbf476dc80ec 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -846,13 +846,21 @@ async def fetch_members(self) -> List[ThreadMember]: members = await self._state.http.get_thread_members(self.id) return [ThreadMember(parent=self, data=data) for data in members] - async def delete(self) -> None: + async def delete(self, *, reason: Optional[str] = None) -> None: """|coro| Deletes this thread. You must have :attr:`~Permissions.manage_threads` to delete threads. + Parameters + ----------- + reason: Optional[:class:`str`] + The reason for deleting this thread. + Shows up on the audit log. + + .. versionadded:: 2.4 + Raises ------- Forbidden @@ -860,7 +868,7 @@ async def delete(self) -> None: HTTPException Deleting the thread failed. """ - await self._state.http.delete_channel(self.id) + await self._state.http.delete_channel(self.id, reason=reason) def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. From 692db7e9ab86f48bebe6e68742c6d0a4fbbaf42d Mon Sep 17 00:00:00 2001 From: fretgfr <51489753+fretgfr@users.noreply.github.com> Date: Sat, 4 May 2024 23:32:52 -0400 Subject: [PATCH 036/354] Add approximate_guild_count to AppInfo --- discord/appinfo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/appinfo.py b/discord/appinfo.py index 79be2035f8fd..074892d051af 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -142,6 +142,10 @@ class AppInfo: redirect_uris: List[:class:`str`] A list of authentication redirect URIs. + .. versionadded:: 2.4 + approximate_guild_count: :class:`int` + The approximate count of the guilds the bot was added to. + .. versionadded:: 2.4 """ @@ -170,6 +174,7 @@ class AppInfo: 'role_connections_verification_url', 'interactions_endpoint_url', 'redirect_uris', + 'approximate_guild_count', ) def __init__(self, state: ConnectionState, data: AppInfoPayload): @@ -206,6 +211,7 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.install_params: Optional[AppInstallParams] = AppInstallParams(params) if params else None self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url') self.redirect_uris: List[str] = data.get('redirect_uris', []) + self.approximate_guild_count: int = data.get('approximate_guild_count', 0) def __repr__(self) -> str: return ( From 9fab99acbc458d27c58b5182fa4e0ad7ef48796f Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 5 May 2024 15:45:32 +1000 Subject: [PATCH 037/354] [commands] Add support for type statement and NewType --- discord/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord/utils.py b/discord/utils.py index 3509bf3ab19e..99c7cfc94233 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -68,6 +68,7 @@ import os import sys import types +import typing import warnings import logging @@ -1080,6 +1081,7 @@ def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]: PY_310 = sys.version_info >= (3, 10) +PY_312 = sys.version_info >= (3, 12) def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: @@ -1118,6 +1120,16 @@ def evaluate_annotation( cache[tp] = evaluated return evaluated + if PY_312 and getattr(tp.__repr__, '__objclass__', None) is typing.TypeAliasType: # type: ignore + temp_locals = dict(**locals, **{t.__name__: t for t in tp.__type_params__}) + annotation = evaluate_annotation(tp.__value__, globals, temp_locals, cache.copy()) + if hasattr(tp, '__args__'): + annotation = annotation[tp.__args__] + return annotation + + if hasattr(tp, '__supertype__'): + return evaluate_annotation(tp.__supertype__, globals, locals, cache) + if hasattr(tp, '__metadata__'): # Annotated[X, Y] can access Y via __metadata__ metadata = tp.__metadata__[0] From 71358b8dcea1923558fb0e51f641d5881e825bf4 Mon Sep 17 00:00:00 2001 From: Vioshim <63890837+Vioshim@users.noreply.github.com> Date: Sun, 5 May 2024 00:48:02 -0500 Subject: [PATCH 038/354] [commands] Add support for positional flag parameters --- discord/ext/commands/flags.py | 37 ++++++++++++++++++++++++++++++++++ docs/ext/commands/commands.rst | 20 +++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index dee00fa7e5c6..8afd29a3d259 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -79,6 +79,10 @@ class Flag: description: :class:`str` The description of the flag. Shown for hybrid commands when they're used as application commands. + positional: :class:`bool` + Whether the flag is positional or not. There can only be one positional flag. + + .. versionadded:: 2.4 """ name: str = MISSING @@ -89,6 +93,7 @@ class Flag: max_args: int = MISSING override: bool = MISSING description: str = MISSING + positional: bool = MISSING cast_to_dict: bool = False @property @@ -109,6 +114,7 @@ def flag( override: bool = MISSING, converter: Any = MISSING, description: str = MISSING, + positional: bool = MISSING, ) -> Any: """Override default functionality and parameters of the underlying :class:`FlagConverter` class attributes. @@ -136,6 +142,10 @@ class attributes. description: :class:`str` The description of the flag. Shown for hybrid commands when they're used as application commands. + positional: :class:`bool` + Whether the flag is positional or not. There can only be one positional flag. + + .. versionadded:: 2.4 """ return Flag( name=name, @@ -145,6 +155,7 @@ class attributes. override=override, annotation=converter, description=description, + positional=positional, ) @@ -171,6 +182,7 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s flags: Dict[str, Flag] = {} cache: Dict[str, Any] = {} names: Set[str] = set() + positional: Optional[Flag] = None for name, annotation in annotations.items(): flag = namespace.pop(name, MISSING) if isinstance(flag, Flag): @@ -183,6 +195,11 @@ def get_flags(namespace: Dict[str, Any], globals: Dict[str, Any], locals: Dict[s if flag.name is MISSING: flag.name = name + if flag.positional: + if positional is not None: + raise TypeError(f"{flag.name!r} positional flag conflicts with {positional.name!r} flag.") + positional = flag + annotation = flag.annotation = resolve_annotation(flag.annotation, globals, locals, cache) if flag.default is MISSING and hasattr(annotation, '__commands_is_flag__') and annotation._can_be_constructible(): @@ -270,6 +287,7 @@ class FlagsMeta(type): __commands_flag_case_insensitive__: bool __commands_flag_delimiter__: str __commands_flag_prefix__: str + __commands_flag_positional__: Optional[Flag] def __new__( cls, @@ -324,9 +342,13 @@ def __new__( delimiter = attrs.setdefault('__commands_flag_delimiter__', ':') prefix = attrs.setdefault('__commands_flag_prefix__', '') + positional: Optional[Flag] = None for flag_name, flag in get_flags(attrs, global_ns, local_ns).items(): flags[flag_name] = flag aliases.update({alias_name: flag_name for alias_name in flag.aliases}) + if flag.positional: + positional = flag + attrs['__commands_flag_positional__'] = positional forbidden = set(delimiter).union(prefix) for flag_name in flags: @@ -500,10 +522,25 @@ def parse_flags(cls, argument: str, *, ignore_extra: bool = True) -> Dict[str, L result: Dict[str, List[str]] = {} flags = cls.__commands_flags__ aliases = cls.__commands_flag_aliases__ + positional_flag = cls.__commands_flag_positional__ last_position = 0 last_flag: Optional[Flag] = None case_insensitive = cls.__commands_flag_case_insensitive__ + + if positional_flag is not None: + match = cls.__commands_flag_regex__.search(argument) + if match is not None: + begin, end = match.span(0) + value = argument[:begin].strip() + else: + value = argument.strip() + last_position = len(argument) + + if value: + name = positional_flag.name.casefold() if case_insensitive else positional_flag.name + result[name] = [value] + for match in cls.__commands_flag_regex__.finditer(argument): begin, end = match.span(0) key = match.group('flag') diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index 02a9ae670baf..52e57ff4dbe9 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -778,6 +778,19 @@ This tells the parser that the ``members`` attribute is mapped to a flag named ` the default value is an empty list. For greater customisability, the default can either be a value or a callable that takes the :class:`~ext.commands.Context` as a sole parameter. This callable can either be a function or a coroutine. +A positional flag can be defined by setting the :attr:`~ext.commands.Flag.positional` attribute to ``True``. This +tells the parser that the content provided before the parsing occurs is part of the flag. This is useful for commands that +require a parameter to be used first and the flags are optional, such as the following: + +.. code-block:: python3 + + class BanFlags(commands.FlagConverter): + members: List[discord.Member] = commands.flag(name='member', positional=True, default=lambda ctx: []) + reason: Optional[str] = None + +.. note:: + Only one positional flag is allowed in a flag converter. + In order to customise the flag syntax we also have a few options that can be passed to the class parameter list: .. code-block:: python3 @@ -796,12 +809,17 @@ In order to customise the flag syntax we also have a few options that can be pas topic: Optional[str] nsfw: Optional[bool] slowmode: Optional[int] + + # Hello there --bold True + class Greeting(commands.FlagConverter): + text: str = commands.flag(positional=True) + bold: bool = False .. note:: Despite the similarities in these examples to command like arguments, the syntax and parser is not a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result - all flags need a corresponding value. + all flags need a corresponding value unless part of a positional flag. Flag converters will only raise :exc:`~ext.commands.FlagError` derived exceptions. If an error is raised while converting a flag, :exc:`~ext.commands.BadFlagArgument` is raised instead and the original exception From 963bb085574c2bb4af1f523ebac03d0366b5a672 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Mon, 6 May 2024 05:36:08 +0200 Subject: [PATCH 039/354] Add support for Message.interaction_metadata Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/message.py | 161 ++++++++++++++++++++++++++++++++-- discord/types/interactions.py | 10 +++ discord/types/message.py | 5 +- docs/interactions/api.rst | 8 ++ 4 files changed, 173 insertions(+), 11 deletions(-) diff --git a/discord/message.py b/discord/message.py index 219d71540c53..a2698abecd40 100644 --- a/discord/message.py +++ b/discord/message.py @@ -56,7 +56,7 @@ from .member import Member from .flags import MessageFlags, AttachmentFlags from .file import File -from .utils import escape_mentions, MISSING +from .utils import escape_mentions, MISSING, deprecated from .http import handle_message_parameters from .guild import Guild from .mixins import Hashable @@ -74,6 +74,7 @@ MessageApplication as MessageApplicationPayload, MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, + MessageInteractionMetadata as MessageInteractionMetadataPayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -109,6 +110,7 @@ 'DeletedReferencedMessage', 'MessageApplication', 'RoleSubscriptionInfo', + 'MessageInteractionMetadata', ) @@ -624,6 +626,123 @@ def created_at(self) -> datetime.datetime: return utils.snowflake_time(self.id) +class MessageInteractionMetadata(Hashable): + """Represents the interaction metadata of a :class:`Message` if + it was sent in response to an interaction. + + .. versionadded:: 2.4 + + .. container:: operations + + .. describe:: x == y + + Checks if two message interactions are equal. + + .. describe:: x != y + + Checks if two message interactions are not equal. + + .. describe:: hash(x) + + Returns the message interaction's hash. + + Attributes + ----------- + id: :class:`int` + The interaction ID. + type: :class:`InteractionType` + The interaction type. + user: :class:`User` + The user that invoked the interaction. + original_response_message_id: Optional[:class:`int`] + The ID of the original response message if the message is a follow-up. + interacted_message_id: Optional[:class:`int`] + 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. + """ + + __slots__: Tuple[str, ...] = ( + 'id', + 'type', + 'user', + 'original_response_message_id', + 'interacted_message_id', + 'modal_interaction', + '_integration_owners', + '_state', + '_guild', + ) + + def __init__(self, *, state: ConnectionState, guild: Optional[Guild], data: MessageInteractionMetadataPayload) -> None: + self._guild: Optional[Guild] = guild + self._state: ConnectionState = state + + self.id: int = int(data['id']) + self.type: InteractionType = try_enum(InteractionType, data['type']) + self.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']) + except KeyError: + pass + + self.interacted_message_id: Optional[int] = None + try: + self.interacted_message_id = int(data['interacted_message_id']) + except KeyError: + pass + + self.modal_interaction: Optional[MessageInteractionMetadata] = None + try: + self.modal_interaction = MessageInteractionMetadata( + state=state, guild=guild, data=data['triggering_interaction_metadata'] + ) + except KeyError: + pass + + def __repr__(self) -> str: + return f'' + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: The interaction's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def original_response_message(self) -> Optional[Message]: + """:class:`~discord.Message`: The original response message if the message + is a follow-up and is found in cache. + """ + if self.original_response_message_id: + return self._state._get_message(self.original_response_message_id) + return None + + @property + def interacted_message(self) -> Optional[Message]: + """:class:`~discord.Message`: The message that + containes the interactive components, if applicable and is found in cache. + """ + if self.interacted_message_id: + return self._state._get_message(self.interacted_message_id) + return None + + def is_guild_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a guild integration.""" + if self._guild: + return self._guild.id == self._integration_owners.get(0) + + return False + + def is_user_integration(self) -> bool: + """:class:`bool`: Returns ``True`` if the interaction is a user integration.""" + return self.user.id == self._integration_owners.get(1) + + def flatten_handlers(cls: Type[Message]) -> Type[Message]: prefix = len('_handle_') handlers = [ @@ -1588,10 +1707,6 @@ class Message(PartialMessage, Hashable): If :attr:`Intents.message_content` is not enabled this will always be an empty list unless the bot is mentioned or the message is a direct message. - .. versionadded:: 2.0 - interaction: Optional[:class:`MessageInteraction`] - The interaction that this message is a response to. - .. versionadded:: 2.0 role_subscription: Optional[:class:`RoleSubscriptionInfo`] The data of the role subscription purchase or renewal that prompted this @@ -1610,6 +1725,10 @@ class Message(PartialMessage, Hashable): .. versionadded:: 2.2 guild: Optional[:class:`Guild`] The guild that the message belongs to, if applicable. + interaction_metadata: Optional[:class:`.MessageInteractionMetadata`] + The metadata of the interaction that this message is a response to. + + .. versionadded:: 2.4 """ __slots__ = ( @@ -1640,10 +1759,11 @@ class Message(PartialMessage, Hashable): 'activity', 'stickers', 'components', - 'interaction', + '_interaction', 'role_subscription', 'application_id', 'position', + 'interaction_metadata', ) if TYPE_CHECKING: @@ -1704,14 +1824,23 @@ def __init__( else: self._thread = Thread(guild=self.guild, state=state, data=thread) - self.interaction: Optional[MessageInteraction] = None + self._interaction: Optional[MessageInteraction] = None + # deprecated try: interaction = data['interaction'] except KeyError: pass else: - self.interaction = MessageInteraction(state=state, guild=self.guild, data=interaction) + self._interaction = MessageInteraction(state=state, guild=self.guild, data=interaction) + + self.interaction_metadata: Optional[MessageInteractionMetadata] = None + try: + interaction_metadata = data['interaction_metadata'] + except KeyError: + pass + else: + self.interaction_metadata = MessageInteractionMetadata(state=state, guild=self.guild, data=interaction_metadata) try: ref = data['message_reference'] @@ -1935,7 +2064,10 @@ def _handle_components(self, data: List[ComponentPayload]) -> None: self.components.append(component) def _handle_interaction(self, data: MessageInteractionPayload): - self.interaction = MessageInteraction(state=self._state, guild=self.guild, data=data) + self._interaction = MessageInteraction(state=self._state, guild=self.guild, data=data) + + def _handle_interaction_metadata(self, data: MessageInteractionMetadataPayload): + self.interaction_metadata = MessageInteractionMetadata(state=self._state, guild=self.guild, data=data) def _rebind_cached_references( self, @@ -2061,6 +2193,17 @@ def thread(self) -> Optional[Thread]: # Fall back to guild threads in case one was created after the message return self._thread or self.guild.get_thread(self.id) + @property + @deprecated("This attribute is deprecated, please use Message.interaction_metadata instead.") + def interaction(self) -> Optional[MessageInteraction]: + """Optional[:class:`~discord.MessageInteraction`]: The interaction that this message is a response to. + + .. versionadded:: 2.0 + .. deprecated:: 2.4 + This attribute is deprecated and will be removed in a future version. Use :attr:`.interaction_metadata` instead. + """ + return self._interaction + def is_system(self) -> bool: """:class:`bool`: Whether the message is a system message. diff --git a/discord/types/interactions.py b/discord/types/interactions.py index d9446ee0eb6f..7aac5df7d095 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -253,3 +253,13 @@ class MessageInteraction(TypedDict): name: str user: User member: NotRequired[Member] + + +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] diff --git a/discord/types/message.py b/discord/types/message.py index 187db715a841..35d80be42193 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -34,7 +34,7 @@ from .embed import Embed from .channel import ChannelType from .components import Component -from .interactions import MessageInteraction +from .interactions import MessageInteraction, MessageInteractionMetadata from .sticker import StickerItem from .threads import Thread @@ -176,7 +176,8 @@ class Message(PartialMessage): flags: NotRequired[int] sticker_items: NotRequired[List[StickerItem]] referenced_message: NotRequired[Optional[Message]] - interaction: NotRequired[MessageInteraction] + interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata + interaction_metadata: NotRequired[MessageInteractionMetadata] components: NotRequired[List[Component]] position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 6aa234257797..53ad210b3e6c 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -45,6 +45,14 @@ MessageInteraction .. autoclass:: MessageInteraction() :members: +MessageInteractionMetadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MessageInteractionMetadata + +.. autoclass:: MessageInteractionMetadata() + :members: + Component ~~~~~~~~~~ From 0c353548e22f53d9c90e073349f06caedbb00d2b Mon Sep 17 00:00:00 2001 From: Thanos <111999343+Sachaa-Thanasius@users.noreply.github.com> Date: Sun, 5 May 2024 23:39:37 -0400 Subject: [PATCH 040/354] Move most static metadata to pyproject.toml --- pyproject.toml | 81 ++++++++++++++++++++++++++++- setup.py | 138 ++++++++++--------------------------------------- 2 files changed, 107 insertions(+), 112 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 559b24b4ba55..d36c85c82741 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,84 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" +[project] +name = "discord.py" +description = "A Python wrapper for the Discord API" +readme = { file = "README.rst", content-type = "text/x-rst" } +license = { file = "LICENSE" } +requires-python = ">=3.8" +authors = [{ name = "Rapptz" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + "Typing :: Typed", +] +dynamic = ["version", "dependencies"] + +[project.urls] +Documentation = "https://discordpy.readthedocs.io/en/latest/" +"Issue tracker" = "https://github.com/Rapptz/discord.py/issues" + +[tool.setuptools.dynamic] +dependencies = { file = "requirements.txt" } + +[project.optional-dependencies] +voice = ["PyNaCl>=1.3.0,<1.6"] +docs = [ + "sphinx==4.4.0", + "sphinxcontrib_trio==1.1.2", + # TODO: bump these when migrating to a newer Sphinx version + "sphinxcontrib-websupport==1.2.4", + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-jsmath==1.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", + "typing-extensions>=4.3,<5", + "sphinx-inline-tabs==2023.4.21", +] +speed = [ + "orjson>=3.5.4", + "aiodns>=1.1", + "Brotli", + "cchardet==2.1.7; python_version < '3.10'", +] +test = [ + "coverage[toml]", + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-mock", + "typing-extensions>=4.3,<5", + "tzdata; sys_platform == 'win32'", +] + +[tool.setuptools] +packages = [ + "discord", + "discord.types", + "discord.ui", + "discord.webhook", + "discord.app_commands", + "discord.ext.commands", + "discord.ext.tasks", +] +include-package-data = true + [tool.black] line-length = 125 skip-string-normalization = true @@ -16,7 +93,7 @@ omit = [ [tool.coverage.report] exclude_lines = [ "pragma: no cover", - "@overload" + "@overload", ] [tool.isort] diff --git a/setup.py b/setup.py index ffe3057fe305..2481afeb428c 100644 --- a/setup.py +++ b/setup.py @@ -1,113 +1,31 @@ from setuptools import setup import re -requirements = [] -with open('requirements.txt') as f: - requirements = f.read().splitlines() - -version = '' -with open('discord/__init__.py') as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) - -if not version: - raise RuntimeError('version is not set') - -if version.endswith(('a', 'b', 'rc')): - # append version identifier based on commit count - try: - import subprocess - - p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - if out: - version += out.decode('utf-8').strip() - p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - if out: - version += '+g' + out.decode('utf-8').strip() - except Exception: - pass - -readme = '' -with open('README.rst') as f: - readme = f.read() - -extras_require = { - 'voice': ['PyNaCl>=1.3.0,<1.6'], - 'docs': [ - 'sphinx==4.4.0', - 'sphinxcontrib_trio==1.1.2', - # TODO: bump these when migrating to a newer Sphinx version - 'sphinxcontrib-websupport==1.2.4', - 'sphinxcontrib-applehelp==1.0.4', - 'sphinxcontrib-devhelp==1.0.2', - 'sphinxcontrib-htmlhelp==2.0.1', - 'sphinxcontrib-jsmath==1.0.1', - 'sphinxcontrib-qthelp==1.0.3', - 'sphinxcontrib-serializinghtml==1.1.5', - 'typing-extensions>=4.3,<5', - 'sphinx-inline-tabs==2023.4.21', - ], - 'speed': [ - 'orjson>=3.5.4', - 'aiodns>=1.1', - 'Brotli', - 'cchardet==2.1.7; python_version < "3.10"', - ], - 'test': [ - 'coverage[toml]', - 'pytest', - 'pytest-asyncio', - 'pytest-cov', - 'pytest-mock', - 'typing-extensions>=4.3,<5', - 'tzdata; sys_platform == "win32"', - ], -} - -packages = [ - 'discord', - 'discord.types', - 'discord.ui', - 'discord.webhook', - 'discord.app_commands', - 'discord.ext.commands', - 'discord.ext.tasks', -] - -setup( - name='discord.py', - author='Rapptz', - url='https://github.com/Rapptz/discord.py', - project_urls={ - 'Documentation': 'https://discordpy.readthedocs.io/en/latest/', - 'Issue tracker': 'https://github.com/Rapptz/discord.py/issues', - }, - version=version, - packages=packages, - license='MIT', - description='A Python wrapper for the Discord API', - long_description=readme, - long_description_content_type='text/x-rst', - include_package_data=True, - install_requires=requirements, - extras_require=extras_require, - python_requires='>=3.8.0', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Internet', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - 'Typing :: Typed', - ], -) +def derive_version() -> str: + version = '' + with open('discord/__init__.py') as f: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) + + if not version: + raise RuntimeError('version is not set') + + if version.endswith(('a', 'b', 'rc')): + # append version identifier based on commit count + try: + import subprocess + + p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += out.decode('utf-8').strip() + p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version += '+g' + out.decode('utf-8').strip() + except Exception: + pass + + return version + + +setup(version=derive_version()) From 2248fc1946b9fd515c7fd0e4a2fbed928172aa22 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Tue, 7 May 2024 00:17:37 +0200 Subject: [PATCH 041/354] Fix various code around Message.interaction(_metadata) --- discord/message.py | 6 +++--- discord/ui/view.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/message.py b/discord/message.py index a2698abecd40..aa1609826832 100644 --- a/discord/message.py +++ b/discord/message.py @@ -715,7 +715,7 @@ def created_at(self) -> datetime.datetime: @property def original_response_message(self) -> Optional[Message]: - """:class:`~discord.Message`: The original response message if the message + """Optional[:class:`~discord.Message`]: The original response message if the message is a follow-up and is found in cache. """ if self.original_response_message_id: @@ -724,7 +724,7 @@ def original_response_message(self) -> Optional[Message]: @property def interacted_message(self) -> Optional[Message]: - """:class:`~discord.Message`: The message that + """Optional[:class:`~discord.Message`]: The message that containes the interactive components, if applicable and is found in cache. """ if self.interacted_message_id: @@ -2194,7 +2194,7 @@ def thread(self) -> Optional[Thread]: return self._thread or self.guild.get_thread(self.id) @property - @deprecated("This attribute is deprecated, please use Message.interaction_metadata instead.") + @deprecated('interaction_metadata') def interaction(self) -> Optional[MessageInteraction]: """Optional[:class:`~discord.MessageInteraction`]: The interaction that this message is a response to. diff --git a/discord/ui/view.py b/discord/ui/view.py index dbe985cd5797..2341a720fef6 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -667,8 +667,8 @@ def dispatch_view(self, component_type: int, custom_id: str, interaction: Intera msg = interaction.message if msg is not None: message_id = msg.id - if msg.interaction: - interaction_id = msg.interaction.id + if msg.interaction_metadata: + interaction_id = msg.interaction_metadata.id key = (component_type, custom_id) From 64e743af5031db6860f99357212afd77a5b029d9 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 6 May 2024 23:03:24 -0400 Subject: [PATCH 042/354] Use InteractionMetadata for InteractionResponse.edit_message --- discord/interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions.py b/discord/interactions.py index 5638886b348f..f471e2040ae7 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -949,7 +949,7 @@ async def edit_message( message_id = msg.id # If this was invoked via an application command then we can use its original interaction ID # Since this is used as a cache key for view updates - original_interaction_id = msg.interaction.id if msg.interaction is not None else None + original_interaction_id = msg.interaction_metadata.id if msg.interaction_metadata is not None else None else: message_id = None original_interaction_id = None From 0bb6967419b772dd408302b72f382b87368cc5d9 Mon Sep 17 00:00:00 2001 From: Willi <83978878+itswilliboy@users.noreply.github.com> Date: Thu, 9 May 2024 05:05:04 +0200 Subject: [PATCH 043/354] Fix typo in Guild.vanity_invite documentation --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 2a23d193fce1..59c216f2f768 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3868,7 +3868,7 @@ async def vanity_invite(self) -> Optional[Invite]: The guild must have ``VANITY_URL`` in :attr:`~Guild.features`. - You must have :attr:`~Permissions.manage_guild` to do this.as well. + You must have :attr:`~Permissions.manage_guild` to do this as well. Raises ------- From a1206dfde841912cbf77ee59a6cbfabf8c707728 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 9 May 2024 05:16:40 -0400 Subject: [PATCH 044/354] Fix typing of various AppCommand decorators --- discord/app_commands/commands.py | 46 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 23fe953a12a5..50a573e8db54 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2523,6 +2523,16 @@ def inner(f: T) -> T: return inner(func) +@overload +def private_channel_only(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def private_channel_only(func: T) -> T: + ... + + def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """A decorator that indicates this command can only be used in the context of DMs and group DMs. @@ -2565,6 +2575,16 @@ def inner(f: T) -> T: return inner(func) +@overload +def dm_only(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def dm_only(func: T) -> T: + ... + + def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """A decorator that indicates this command can only be used in the context of bot DMs. @@ -2606,9 +2626,7 @@ def inner(f: T) -> T: return inner(func) -def allowed_contexts( - guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING -) -> Union[T, Callable[[T], T]]: +def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channels: bool = MISSING) -> Callable[[T], T]: """A decorator that indicates this command can only be used in certain contexts. Valid contexts are guilds, DMs and private channels. @@ -2650,6 +2668,16 @@ def inner(f: T) -> T: return inner +@overload +def guild_install(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def guild_install(func: T) -> T: + ... + + def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """A decorator that indicates this command should be installed in guilds. @@ -2688,6 +2716,16 @@ def inner(f: T) -> T: return inner(func) +@overload +def user_install(func: None = ...) -> Callable[[T], T]: + ... + + +@overload +def user_install(func: T) -> T: + ... + + def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """A decorator that indicates this command should be installed for users. @@ -2729,7 +2767,7 @@ def inner(f: T) -> T: def allowed_installs( guilds: bool = MISSING, users: bool = MISSING, -) -> Union[T, Callable[[T], T]]: +) -> Callable[[T], T]: """A decorator that indicates this command should be installed in certain contexts. Valid contexts are guilds and users. From e43bd8692cf3e293e5fd1bbe7ec481dd8aa09c4a Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 10 May 2024 12:14:12 +0200 Subject: [PATCH 045/354] Add support for Polls Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Josh <8677174+bijij@users.noreply.github.com> Co-authored-by: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> --- discord/__init__.py | 1 + discord/abc.py | 14 + discord/client.py | 26 ++ discord/enums.py | 5 + discord/ext/commands/context.py | 17 + discord/flags.py | 51 +++ discord/http.py | 43 +++ discord/interactions.py | 3 + discord/message.py | 43 +++ discord/permissions.py | 18 + discord/poll.py | 571 ++++++++++++++++++++++++++++++++ discord/raw_models.py | 32 ++ discord/state.py | 60 ++++ discord/types/gateway.py | 8 + discord/types/message.py | 2 + discord/types/poll.py | 88 +++++ discord/webhook/async_.py | 21 ++ discord/webhook/sync.py | 22 +- docs/api.rst | 73 ++++ 19 files changed, 1097 insertions(+), 1 deletion(-) create mode 100644 discord/poll.py create mode 100644 discord/types/poll.py diff --git a/discord/__init__.py b/discord/__init__.py index d239c8f3b383..e3148e51378b 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -69,6 +69,7 @@ from .components import * from .threads import * from .automod import * +from .poll import * class VersionInfo(NamedTuple): diff --git a/discord/abc.py b/discord/abc.py index 8eeb9d4d0f58..656a38659f90 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -92,6 +92,7 @@ VoiceChannel, StageChannel, ) + from .poll import Poll from .threads import Thread from .ui.view import View from .types.channel import ( @@ -1350,6 +1351,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1370,6 +1372,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1390,6 +1393,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1410,6 +1414,7 @@ async def send( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1431,6 +1436,7 @@ async def send( view: Optional[View] = None, suppress_embeds: bool = False, silent: bool = False, + poll: Optional[Poll] = None, ) -> Message: """|coro| @@ -1516,6 +1522,10 @@ async def send( in the UI, but will not actually send a notification. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 Raises -------- @@ -1582,6 +1592,7 @@ async def send( stickers=sticker_ids, view=view, flags=flags, + poll=poll, ) as params: data = await state.http.send_message(channel.id, params=params) @@ -1589,6 +1600,9 @@ async def send( if view and not view.is_finished(): state.store_view(view, ret.id) + if poll: + poll._update(ret) + if delete_after is not None: await ret.delete(delay=delete_after) return ret diff --git a/discord/client.py b/discord/client.py index f452ca30a50f..a91be7160d20 100644 --- a/discord/client.py +++ b/discord/client.py @@ -107,6 +107,7 @@ RawThreadMembersUpdate, RawThreadUpdateEvent, RawTypingEvent, + RawPollVoteActionEvent, ) from .reaction import Reaction from .role import Role @@ -116,6 +117,7 @@ from .ui.item import Item from .voice_client import VoiceProtocol from .audit_logs import AuditLogEntry + from .poll import PollAnswer # fmt: off @@ -1815,6 +1817,30 @@ async def wait_for( ) -> Tuple[Member, VoiceState, VoiceState]: ... + # Polls + + @overload + async def wait_for( + self, + event: Literal['poll_vote_add', 'poll_vote_remove'], + /, + *, + check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = None, + timeout: Optional[float] = None, + ) -> Tuple[Union[User, Member], PollAnswer]: + ... + + @overload + async def wait_for( + self, + event: Literal['raw_poll_vote_add', 'raw_poll_vote_remove'], + /, + *, + check: Optional[Callable[[RawPollVoteActionEvent], bool]] = None, + timeout: Optional[float] = None, + ) -> RawPollVoteActionEvent: + ... + # Commands @overload diff --git a/discord/enums.py b/discord/enums.py index f1af2d790500..f7989a195e0a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -73,6 +73,7 @@ 'SKUType', 'EntitlementType', 'EntitlementOwnerType', + 'PollLayoutType', ) @@ -818,6 +819,10 @@ class EntitlementOwnerType(Enum): user = 2 +class PollLayoutType(Enum): + default = 1 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index d4052cbbd914..ad9c286eef57 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -50,6 +50,7 @@ from discord.message import MessageReference, PartialMessage from discord.ui import View from discord.types.interactions import ApplicationCommandInteractionData + from discord.poll import Poll from .cog import Cog from .core import Command @@ -641,6 +642,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -662,6 +664,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -683,6 +686,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -704,6 +708,7 @@ async def reply( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -826,6 +831,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -847,6 +853,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -868,6 +875,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -889,6 +897,7 @@ async def send( suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -911,6 +920,7 @@ async def send( suppress_embeds: bool = False, ephemeral: bool = False, silent: bool = False, + poll: Poll = MISSING, ) -> Message: """|coro| @@ -1000,6 +1010,11 @@ async def send( .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 + Raises -------- ~discord.HTTPException @@ -1037,6 +1052,7 @@ async def send( view=view, suppress_embeds=suppress_embeds, silent=silent, + poll=poll, ) # type: ignore # The overloads don't support Optional but the implementation does # Convert the kwargs from None to MISSING to appease the remaining implementations @@ -1052,6 +1068,7 @@ async def send( 'suppress_embeds': suppress_embeds, 'ephemeral': ephemeral, 'silent': silent, + 'poll': poll, } if self.interaction.response.is_done(): diff --git a/discord/flags.py b/discord/flags.py index 249c2e8f6c12..3d31e3a58a0c 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -1257,6 +1257,57 @@ def auto_moderation_execution(self): """ return 1 << 21 + @alias_flag_value + def polls(self): + """:class:`bool`: Whether guild and direct messages poll related events are enabled. + + This is a shortcut to set or get both :attr:`guild_polls` and :attr:`dm_polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (both guilds and DMs) + - :func:`on_poll_vote_remove` (both guilds and DMs) + - :func:`on_raw_poll_vote_add` (both guilds and DMs) + - :func:`on_raw_poll_vote_remove` (both guilds and DMs) + + .. versionadded:: 2.4 + """ + return (1 << 24) | (1 << 25) + + @flag_value + def guild_polls(self): + """:class:`bool`: Whether guild poll related events are enabled. + + See also :attr:`dm_polls` and :attr:`polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for guilds) + - :func:`on_poll_vote_remove` (only for guilds) + - :func:`on_raw_poll_vote_add` (only for guilds) + - :func:`on_raw_poll_vote_remove` (only for guilds) + + .. versionadded:: 2.4 + """ + return 1 << 24 + + @flag_value + def dm_polls(self): + """:class:`bool`: Whether direct messages poll related events are enabled. + + See also :attr:`guild_polls` and :attr:`polls`. + + This corresponds to the following events: + + - :func:`on_poll_vote_add` (only for DMs) + - :func:`on_poll_vote_remove` (only for DMs) + - :func:`on_raw_poll_vote_add` (only for DMs) + - :func:`on_raw_poll_vote_remove` (only for DMs) + + .. versionadded:: 2.4 + """ + return 1 << 25 + @fill_with_flags() class MemberCacheFlags(BaseFlags): diff --git a/discord/http.py b/discord/http.py index f36d191e4b0e..aab710580c53 100644 --- a/discord/http.py +++ b/discord/http.py @@ -68,6 +68,7 @@ from .embeds import Embed from .message import Attachment from .flags import MessageFlags + from .poll import Poll from .types import ( appinfo, @@ -91,6 +92,7 @@ sticker, welcome_screen, sku, + poll, ) from .types.snowflake import Snowflake, SnowflakeList @@ -154,6 +156,7 @@ def handle_message_parameters( thread_name: str = MISSING, channel_payload: Dict[str, Any] = MISSING, applied_tags: Optional[SnowflakeList] = MISSING, + poll: Optional[Poll] = MISSING, ) -> MultipartParameters: if files is not MISSING and file is not MISSING: raise TypeError('Cannot mix file and files keyword arguments.') @@ -256,6 +259,9 @@ def handle_message_parameters( } payload.update(channel_payload) + if poll not in (MISSING, None): + payload['poll'] = poll._to_dict() # type: ignore + multipart = [] if files: multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)}) @@ -2513,6 +2519,43 @@ def edit_application_info(self, *, reason: Optional[str], payload: Any) -> Respo payload = {k: v for k, v in payload.items() if k in valid_keys} return self.request(Route('PATCH', '/applications/@me'), json=payload, reason=reason) + def get_poll_answer_voters( + self, + channel_id: Snowflake, + message_id: Snowflake, + answer_id: Snowflake, + after: Optional[Snowflake] = None, + limit: Optional[int] = None, + ) -> Response[poll.PollAnswerVoters]: + params = {} + + if after: + params['after'] = int(after) + + if limit is not None: + params['limit'] = limit + + return self.request( + Route( + 'GET', + '/channels/{channel_id}/polls/{message_id}/answers/{answer_id}', + channel_id=channel_id, + message_id=message_id, + answer_id=answer_id, + ), + params=params, + ) + + def end_poll(self, channel_id: Snowflake, message_id: Snowflake) -> Response[message.Message]: + return self.request( + Route( + 'POST', + '/channels/{channel_id}/polls/{message_id}/expire', + channel_id=channel_id, + message_id=message_id, + ) + ) + async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: try: data = await self.request(Route('GET', '/gateway')) diff --git a/discord/interactions.py b/discord/interactions.py index f471e2040ae7..5702e8b8d7b6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -78,6 +78,7 @@ from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel from .threads import Thread from .app_commands.commands import Command, ContextMenu + from .poll import Poll InteractionChannel = Union[ VoiceChannel, @@ -762,6 +763,7 @@ async def send_message( suppress_embeds: bool = False, silent: bool = False, delete_after: Optional[float] = None, + poll: Poll = MISSING, ) -> None: """|coro| @@ -842,6 +844,7 @@ async def send_message( allowed_mentions=allowed_mentions, flags=flags, view=view, + poll=poll, ) http = parent._state.http diff --git a/discord/message.py b/discord/message.py index aa1609826832..ea62b87f6784 100644 --- a/discord/message.py +++ b/discord/message.py @@ -63,6 +63,7 @@ from .sticker import StickerItem, GuildSticker from .threads import Thread from .channel import PartialMessageable +from .poll import Poll if TYPE_CHECKING: from typing_extensions import Self @@ -1464,6 +1465,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1484,6 +1486,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1504,6 +1507,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1524,6 +1528,7 @@ async def reply( view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., + poll: Poll = ..., ) -> Message: ... @@ -1558,6 +1563,30 @@ async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: return await self.channel.send(content, reference=self, **kwargs) + async def end_poll(self) -> Message: + """|coro| + + Ends the :class:`Poll` attached to this message. + + This can only be done if you are the message author. + + If the poll was successfully ended, then it returns the updated :class:`Message`. + + Raises + ------ + ~discord.HTTPException + Ending the poll failed. + + Returns + ------- + :class:`.Message` + The updated message. + """ + + data = await self._state.http.end_poll(self.channel.id, self.id) + + return Message(state=self._state, channel=self.channel, data=data) + def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: """Creates a :class:`~discord.MessageReference` from the current message. @@ -1728,6 +1757,10 @@ class Message(PartialMessage, Hashable): interaction_metadata: Optional[:class:`.MessageInteractionMetadata`] The metadata of the interaction that this message is a response to. + .. versionadded:: 2.4 + poll: Optional[:class:`Poll`] + The poll attached to this message. + .. versionadded:: 2.4 """ @@ -1764,6 +1797,7 @@ class Message(PartialMessage, Hashable): 'application_id', 'position', 'interaction_metadata', + 'poll', ) if TYPE_CHECKING: @@ -1803,6 +1837,15 @@ def __init__( self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] + # This updates the poll so it has the counts, if the message + # was previously cached. + self.poll: Optional[Poll] = state._get_poll(self.id) + if self.poll is None: + try: + self.poll = Poll._from_data(data=data['poll'], message=self, state=state) + except KeyError: + pass + try: # if the channel doesn't have a guild attribute, we handle that self.guild = channel.guild diff --git a/discord/permissions.py b/discord/permissions.py index f18f94a7a11d..916fa4d2fe30 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -730,6 +730,22 @@ def send_voice_messages(self) -> int: """ return 1 << 46 + @flag_value + def send_polls(self) -> int: + """:class:`bool`: Returns ``True`` if a user can send poll messages. + + .. versionadded:: 2.4 + """ + return 1 << 49 + + @make_permission_alias('send_polls') + def create_polls(self) -> int: + """:class:`bool`: An alias for :attr:`send_polls`. + + .. versionadded:: 2.4 + """ + return 1 << 49 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -850,6 +866,8 @@ class PermissionOverwrite: send_voice_messages: Optional[bool] create_expressions: Optional[bool] create_events: Optional[bool] + send_polls: Optional[bool] + create_polls: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/poll.py b/discord/poll.py new file mode 100644 index 000000000000..f9b2d04c53a7 --- /dev/null +++ b/discord/poll.py @@ -0,0 +1,571 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + + +from typing import Optional, List, TYPE_CHECKING, Union, AsyncIterator, Dict + +import datetime + +from .enums import PollLayoutType, try_enum +from . import utils +from .emoji import PartialEmoji, Emoji +from .user import User +from .object import Object +from .errors import ClientException + +if TYPE_CHECKING: + from typing_extensions import Self + + from .message import Message + from .abc import Snowflake + from .state import ConnectionState + from .member import Member + + from .types.poll import ( + PollCreate as PollCreatePayload, + PollMedia as PollMediaPayload, + PollAnswerCount as PollAnswerCountPayload, + Poll as PollPayload, + PollAnswerWithID as PollAnswerWithIDPayload, + PollResult as PollResultPayload, + PollAnswer as PollAnswerPayload, + ) + + +__all__ = ( + 'Poll', + 'PollAnswer', + 'PollMedia', +) + +MISSING = utils.MISSING +PollMediaEmoji = Union[PartialEmoji, Emoji, str] + + +class PollMedia: + """Represents the poll media for a poll item. + + .. versionadded:: 2.4 + + Attributes + ---------- + text: :class:`str` + The displayed text. + emoji: Optional[Union[:class:`PartialEmoji`, :class:`Emoji`]] + The attached emoji for this media. This is only valid for poll answers. + """ + + __slots__ = ('text', 'emoji') + + def __init__(self, /, text: str, emoji: Optional[PollMediaEmoji] = None) -> None: + self.text: str = text + self.emoji: Optional[Union[PartialEmoji, Emoji]] = PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji + + def __repr__(self) -> str: + return f'' + + def to_dict(self) -> PollMediaPayload: + payload: PollMediaPayload = {'text': self.text} + + if self.emoji is not None: + payload['emoji'] = self.emoji._to_partial().to_dict() + + return payload + + @classmethod + def from_dict(cls, *, data: PollMediaPayload) -> Self: + emoji = data.get('emoji') + + if emoji: + return cls(text=data['text'], emoji=PartialEmoji.from_dict(emoji)) + return cls(text=data['text']) + + +class PollAnswer: + """Represents a poll's answer. + + .. container:: operations + + .. describe:: str(x) + + Returns this answer's text, if any. + + .. versionadded:: 2.4 + + Attributes + ---------- + id: :class:`int` + The ID of this answer. + media: :class:`PollMedia` + The display data for this answer. + self_voted: :class:`bool` + Whether the current user has voted to this answer or not. + """ + + __slots__ = ('media', 'id', '_state', '_message', '_vote_count', 'self_voted', '_poll') + + def __init__( + self, + *, + message: Optional[Message], + poll: Poll, + data: PollAnswerWithIDPayload, + ) -> None: + self.media: PollMedia = PollMedia.from_dict(data=data['poll_media']) + self.id: int = int(data['answer_id']) + self._message: Optional[Message] = message + self._state: Optional[ConnectionState] = message._state if message else None + self._vote_count: int = 0 + self.self_voted: bool = False + self._poll: Poll = poll + + def _handle_vote_event(self, added: bool, self_voted: bool) -> None: + if added: + self._vote_count += 1 + else: + self._vote_count -= 1 + self.self_voted = self_voted + + def _update_with_results(self, payload: PollAnswerCountPayload) -> None: + self._vote_count = int(payload['count']) + self.self_voted = payload['me_voted'] + + def __str__(self) -> str: + return self.media.text + + def __repr__(self) -> str: + return f'' + + @classmethod + def from_params( + cls, + id: int, + text: str, + emoji: Optional[PollMediaEmoji] = None, + *, + poll: Poll, + message: Optional[Message], + ) -> Self: + poll_media: PollMediaPayload = {'text': text} + if emoji is not None: + emoji = PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji._to_partial() + emoji_data = emoji.to_dict() + # No need to remove animated key as it will be ignored + poll_media['emoji'] = emoji_data + + payload: PollAnswerWithIDPayload = {'answer_id': id, 'poll_media': poll_media} + + return cls(data=payload, message=message, poll=poll) + + @property + def text(self) -> str: + """:class:`str`: Returns this answer's displayed text.""" + return self.media.text + + @property + def emoji(self) -> Optional[Union[PartialEmoji, Emoji]]: + """Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]: Returns this answer's displayed + emoji, if any. + """ + return self.media.emoji + + @property + def vote_count(self) -> int: + """:class:`int`: Returns an approximate count of votes for this answer. + + If the poll is finished, the count is exact. + """ + return self._vote_count + + @property + def poll(self) -> Poll: + """:class:`Poll`: Returns the parent poll of this answer""" + return self._poll + + def _to_dict(self) -> PollAnswerPayload: + return { + 'poll_media': self.media.to_dict(), + } + + async def voters( + self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None + ) -> AsyncIterator[Union[User, Member]]: + """Returns an :term:`asynchronous iterator` representing the users that have voted on this answer. + + The ``after`` parameter must represent a user + and meet the :class:`abc.Snowflake` abc. + + This can only be called when the parent poll was sent to a message. + + Examples + -------- + + Usage :: + + async for voter in poll_answer.voters(): + print(f'{voter} has voted for {poll_answer}!') + + Flattening into a list: :: + + voters = [voter async for voter in poll_answer.voters()] + # voters is now a list of User + + Parameters + ---------- + limit: Optional[:class:`int`] + The maximum number of results to return. + If not provided, returns all the users who + voted on this poll answer. + after: Optional[:class:`abc.Snowflake`] + For pagination, voters are sorted by member. + + Raises + ------ + HTTPException + Retrieving the users failed. + + Yields + ------ + Union[:class:`User`, :class:`Member`] + The member (if retrievable) or the user that has voted + on this poll answer. The case where it can be a :class:`Member` + is in a guild message context. Sometimes it can be a :class:`User` + if the member has left the guild or if the member is not cached. + """ + + if not self._message or not self._state: # Make type checker happy + raise ClientException('You cannot fetch users to a poll not sent with a message') + + if limit is None: + if not self._message.poll: + limit = 100 + else: + limit = self.vote_count or 100 + + while limit > 0: + retrieve = min(limit, 100) + + message = self._message + guild = self._message.guild + state = self._state + after_id = after.id if after else None + + data = await state.http.get_poll_answer_voters( + message.channel.id, message.id, self.id, after=after_id, limit=retrieve + ) + users = data['users'] + + if len(users) == 0: + # No more voters to fetch, terminate loop + break + + limit -= len(users) + after = Object(id=int(users[-1]['id'])) + + if not guild or isinstance(guild, Object): + for raw_user in reversed(users): + yield User(state=self._state, data=raw_user) + continue + + for raw_member in reversed(users): + member_id = int(raw_member['id']) + member = guild.get_member(member_id) + + yield member or User(state=self._state, data=raw_member) + + +class Poll: + """Represents a message's Poll. + + .. versionadded:: 2.4 + + Parameters + ---------- + question: Union[:class:`PollMedia`, :class:`str`] + The poll's displayed question. The text can be up to 300 characters. + duration: :class:`datetime.timedelta` + The duration of the poll. Duration must be in hours. + multiple: :class:`bool` + Whether users are allowed to select more than one answer. + Defaultsto ``False``. + layout_type: :class:`PollLayoutType` + The layout type of the poll. Defaults to :attr:`PollLayoutType.default`. + """ + + __slots__ = ( + 'multiple', + '_answers', + 'duration', + 'layout_type', + '_question_media', + '_message', + '_expiry', + '_finalized', + '_state', + ) + + def __init__( + self, + question: Union[PollMedia, str], + duration: datetime.timedelta, + *, + multiple: bool = False, + layout_type: PollLayoutType = PollLayoutType.default, + ) -> None: + self._question_media: PollMedia = PollMedia(text=question, emoji=None) if isinstance(question, str) else question + self._answers: Dict[int, PollAnswer] = {} + self.duration: datetime.timedelta = duration + + self.multiple: bool = multiple + self.layout_type: PollLayoutType = layout_type + + # NOTE: These attributes are set manually when calling + # _from_data, so it should be ``None`` now. + self._message: Optional[Message] = None + self._state: Optional[ConnectionState] = None + self._finalized: bool = False + self._expiry: Optional[datetime.datetime] = None + + def _update(self, message: Message) -> None: + self._state = message._state + self._message = message + + if not message.poll: + return + + # The message's poll contains the more up to date data. + self._expiry = message.poll.expires_at + self._finalized = message.poll._finalized + + def _update_results(self, data: PollResultPayload) -> None: + self._finalized = data['is_finalized'] + + for count in data['answer_counts']: + answer = self.get_answer(int(count['id'])) + if not answer: + continue + + answer._update_with_results(count) + + def _handle_vote(self, answer_id: int, added: bool, self_voted: bool = False): + answer = self.get_answer(answer_id) + if not answer: + return + + answer._handle_vote_event(added, self_voted) + + @classmethod + def _from_data(cls, *, data: PollPayload, message: Message, state: ConnectionState) -> Self: + multiselect = data.get('allow_multiselect', False) + layout_type = try_enum(PollLayoutType, data.get('layout_type', 1)) + question_data = data.get('question') + question = question_data.get('text') + expiry = utils.parse_time(data['expiry']) # If obtained via API, then expiry is set. + duration = expiry - message.created_at + # self.created_at = message.created_at + # duration = self.created_at - expiry + + if (duration.total_seconds() / 3600) > 168: # As the duration may exceed little milliseconds then we fix it + duration = datetime.timedelta(days=7) + + self = cls( + duration=duration, + multiple=multiselect, + layout_type=layout_type, + question=question, + ) + self._answers = { + int(answer['answer_id']): PollAnswer(data=answer, message=message, poll=self) for answer in data['answers'] + } + self._message = message + self._state = state + self._expiry = expiry + + try: + self._update_results(data['results']) + except KeyError: + pass + + return self + + def _to_dict(self) -> PollCreatePayload: + data: PollCreatePayload = { + 'allow_multiselect': self.multiple, + 'question': self._question_media.to_dict(), + 'duration': self.duration.total_seconds() / 3600, + 'layout_type': self.layout_type.value, + 'answers': [answer._to_dict() for answer in self.answers], + } + return data + + def __repr__(self) -> str: + return f"" + + @property + def question(self) -> str: + """:class:`str`: Returns this poll answer question string.""" + return self._question_media.text + + @property + def answers(self) -> List[PollAnswer]: + """List[:class:`PollAnswer`]: Returns a read-only copy of the answers""" + return list(self._answers.values()) + + @property + def expires_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry. + + .. note:: + + This will **always** be ``None`` for stateless polls. + """ + return self._expiry + + @property + def created_at(self) -> Optional[datetime.datetime]: + """:class:`datetime.datetime`: Returns the poll's creation time, or ``None`` if user-created.""" + + if not self._message: + return + return self._message.created_at + + @property + def message(self) -> Optional[Message]: + """:class:`Message`: The message this poll is from.""" + return self._message + + @property + def total_votes(self) -> int: + """:class:`int`: Returns the sum of all the answer votes.""" + return sum([answer.vote_count for answer in self.answers]) + + def is_finalised(self) -> bool: + """:class:`bool`: Returns whether the poll has finalised. + + This always returns ``False`` for stateless polls. + """ + return self._finalized + + is_finalized = is_finalised + + def copy(self) -> Self: + """Returns a stateless copy of this poll. + + This is meant to be used when you want to edit a stateful poll. + + Returns + ------- + :class:`Poll` + The copy of the poll. + """ + + new = self.__class__(question=self.question, duration=self.duration) + + # We want to return a stateless copy of the poll, so we should not + # override new._answers as our answers may contain a state + for answer in self.answers: + new.add_answer(text=answer.text, emoji=answer.emoji) + + return new + + def add_answer( + self, + *, + text: str, + emoji: Optional[Union[PartialEmoji, Emoji, str]] = None, + ) -> Self: + """Appends a new answer to this poll. + + Parameters + ---------- + text: :class:`str` + The text label for this poll answer. Can be up to 55 + characters. + emoji: Union[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`] + The emoji to display along the text. + + Raises + ------ + ClientException + Cannot append answers to a poll that is active. + + Returns + ------- + :class:`Poll` + This poll with the new answer appended. This allows fluent-style chaining. + """ + + if self._message: + raise ClientException('Cannot append answers to a poll that is active') + + answer = PollAnswer.from_params(id=len(self.answers) + 1, text=text, emoji=emoji, message=self._message, poll=self) + self._answers[answer.id] = answer + return self + + def get_answer( + self, + /, + id: int, + ) -> Optional[PollAnswer]: + """Returns the answer with the provided ID or ``None`` if not found. + + Parameters + ---------- + id: :class:`int` + The ID of the answer to get. + + Returns + ------- + Optional[:class:`PollAnswer`] + The answer. + """ + + return self._answers.get(id) + + async def end(self) -> Self: + """|coro| + + Ends the poll. + + Raises + ------ + ClientException + This poll has no attached message. + HTTPException + Ending the poll failed. + + Returns + ------- + :class:`Poll` + The updated poll. + """ + + if not self._message or not self._state: # Make type checker happy + raise ClientException('This poll has no attached message.') + + self._message = await self._message.end_poll() + + return self diff --git a/discord/raw_models.py b/discord/raw_models.py index 2fd94539e1a6..571be38f1f5a 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -49,6 +49,7 @@ ThreadMembersUpdate, TypingStartEvent, GuildMemberRemoveEvent, + PollVoteActionEvent, ) from .types.command import GuildApplicationCommandPermissions from .message import Message @@ -77,6 +78,7 @@ 'RawTypingEvent', 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', + 'RawPollVoteActionEvent', ) @@ -519,3 +521,33 @@ def __init__(self, *, data: GuildApplicationCommandPermissions, state: Connectio self.permissions: List[AppCommandPermissions] = [ AppCommandPermissions(data=perm, guild=self.guild, state=state) for perm in data['permissions'] ] + + +class RawPollVoteActionEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_poll_vote_add` or :func:`on_raw_poll_vote_remove` + event. + + .. versionadded:: 2.4 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that added or removed a vote. + channel_id: :class:`int` + The channel ID where the poll vote action took place. + message_id: :class:`int` + The message ID that contains the poll the user added or removed their vote on. + guild_id: Optional[:class:`int`] + The guild ID where the vote got added or removed, if applicable.. + answer_id: :class:`int` + The poll answer's ID the user voted on. + """ + + __slots__ = ('user_id', 'channel_id', 'message_id', 'guild_id', 'answer_id') + + def __init__(self, data: PollVoteActionEvent) -> None: + self.user_id: int = int(data['user_id']) + self.channel_id: int = int(data['channel_id']) + self.message_id: int = int(data['message_id']) + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.answer_id: int = int(data['answer_id']) diff --git a/discord/state.py b/discord/state.py index a966cb667017..032dc2645c7c 100644 --- a/discord/state.py +++ b/discord/state.py @@ -89,6 +89,7 @@ from .ui.item import Item from .ui.dynamic import DynamicItem from .app_commands import CommandTree, Translator + from .poll import Poll from .types.automod import AutoModerationRule, AutoModerationActionExecution from .types.snowflake import Snowflake @@ -509,6 +510,12 @@ def _remove_private_channel(self, channel: PrivateChannel) -> None: def _get_message(self, msg_id: Optional[int]) -> Optional[Message]: return utils.find(lambda m: m.id == msg_id, reversed(self._messages)) if self._messages else None + def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: + message = self._get_message(msg_id) + if not message: + return + return message.poll + def _add_guild_from_data(self, data: GuildPayload) -> Guild: guild = Guild(data=data, state=self) self._add_guild(guild) @@ -533,6 +540,13 @@ def _get_guild_channel( return channel or PartialMessageable(state=self, guild_id=guild_id, id=channel_id), guild + def _update_poll_counts(self, message: Message, answer_id: int, added: bool, self_voted: bool = False) -> Optional[Poll]: + poll = message.poll + if not poll: + return + poll._handle_vote(answer_id, added, self_voted) + return poll + async def chunker( self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None ) -> None: @@ -1619,6 +1633,52 @@ def parse_entitlement_delete(self, data: gw.EntitlementDeleteEvent) -> None: entitlement = Entitlement(data=data, state=self) self.dispatch('entitlement_delete', entitlement) + def parse_message_poll_vote_add(self, data: gw.PollVoteActionEvent) -> None: + raw = RawPollVoteActionEvent(data) + + self.dispatch('raw_poll_vote_add', raw) + + message = self._get_message(raw.message_id) + guild = self._get_guild(raw.guild_id) + + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + + if message and user: + poll = self._update_poll_counts(message, raw.answer_id, True, raw.user_id == self.self_id) + if not poll: + _log.warning( + 'POLL_VOTE_ADD referencing message with ID: %s does not have a poll. Discarding.', raw.message_id + ) + return + + self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) + + def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: + raw = RawPollVoteActionEvent(data) + + self.dispatch('raw_poll_vote_remove', raw) + + message = self._get_message(raw.message_id) + guild = self._get_guild(raw.guild_id) + + if guild: + user = guild.get_member(raw.user_id) + else: + user = self.get_user(raw.user_id) + + if message and user: + poll = self._update_poll_counts(message, raw.answer_id, False, raw.user_id == self.self_id) + if not poll: + _log.warning( + 'POLL_VOTE_REMOVE referencing message with ID: %s does not have a poll. Discarding.', raw.message_id + ) + return + + self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) + def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): return channel.guild.get_member(user_id) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index c0908435f9f2..b79bd9ca9fb3 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -352,3 +352,11 @@ class GuildAuditLogEntryCreate(AuditLogEntry): EntitlementCreateEvent = EntitlementUpdateEvent = EntitlementDeleteEvent = Entitlement + + +class PollVoteActionEvent(TypedDict): + user_id: Snowflake + channel_id: Snowflake + message_id: Snowflake + guild_id: NotRequired[Snowflake] + answer_id: int diff --git a/discord/types/message.py b/discord/types/message.py index 35d80be42193..16912d628715 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -37,6 +37,7 @@ from .interactions import MessageInteraction, MessageInteractionMetadata from .sticker import StickerItem from .threads import Thread +from .poll import Poll class PartialMessage(TypedDict): @@ -163,6 +164,7 @@ class Message(PartialMessage): attachments: List[Attachment] embeds: List[Embed] pinned: bool + poll: NotRequired[Poll] type: MessageType member: NotRequired[Member] mention_channels: NotRequired[List[ChannelMention]] diff --git a/discord/types/poll.py b/discord/types/poll.py new file mode 100644 index 000000000000..fabdbd48f7ad --- /dev/null +++ b/discord/types/poll.py @@ -0,0 +1,88 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + + +from typing import List, TypedDict, Optional, Literal, TYPE_CHECKING +from typing_extensions import NotRequired + +from .snowflake import Snowflake + +if TYPE_CHECKING: + from .user import User + from .emoji import PartialEmoji + + +LayoutType = Literal[1] # 1 = Default + + +class PollMedia(TypedDict): + text: str + emoji: NotRequired[Optional[PartialEmoji]] + + +class PollAnswer(TypedDict): + poll_media: PollMedia + + +class PollAnswerWithID(PollAnswer): + answer_id: int + + +class PollAnswerCount(TypedDict): + id: Snowflake + count: int + me_voted: bool + + +class PollAnswerVoters(TypedDict): + users: List[User] + + +class PollResult(TypedDict): + is_finalized: bool + answer_counts: List[PollAnswerCount] + + +class PollCreate(TypedDict): + allow_multiselect: bool + answers: List[PollAnswer] + duration: float + layout_type: LayoutType + question: PollMedia + + +# We don't subclass Poll as it will +# still have the duration field, which +# is converted into expiry when poll is +# fetched from a message or returned +# by a `send` method in a Messageable +class Poll(TypedDict): + allow_multiselect: bool + answers: List[PollAnswerWithID] + expiry: str + layout_type: LayoutType + question: PollMedia + results: PollResult diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 767db38cc6a8..d04e21b57d55 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -72,6 +72,7 @@ from ..channel import VoiceChannel from ..abc import Snowflake from ..ui.view import View + from ..poll import Poll import datetime from ..types.webhook import ( Webhook as WebhookPayload, @@ -541,6 +542,7 @@ def interaction_message_response_params( view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, ) -> MultipartParameters: if files is not MISSING and file is not MISSING: raise TypeError('Cannot mix file and files keyword arguments.') @@ -608,6 +610,9 @@ def interaction_message_response_params( data['attachments'] = attachments_payload + if poll is not MISSING: + data['poll'] = poll._to_dict() + multipart = [] if files: data = {'type': type, 'data': data} @@ -1597,6 +1602,7 @@ async def send( suppress_embeds: bool = MISSING, silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> WebhookMessage: ... @@ -1621,6 +1627,7 @@ async def send( suppress_embeds: bool = MISSING, silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> None: ... @@ -1644,6 +1651,7 @@ async def send( suppress_embeds: bool = False, silent: bool = False, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> Optional[WebhookMessage]: """|coro| @@ -1734,6 +1742,15 @@ async def send( .. versionadded:: 2.4 + poll: :class:`Poll` + The poll to send with this message. + + .. warning:: + + When sending a Poll via webhook, you cannot manually end it. + + .. versionadded:: 2.4 + Raises -------- HTTPException @@ -1811,6 +1828,7 @@ async def send( allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, applied_tags=applied_tag_ids, + poll=poll, ) as params: adapter = async_context.get() thread_id: Optional[int] = None @@ -1838,6 +1856,9 @@ async def send( message_id = None if msg is None else msg.id self._state.store_view(view, message_id) + if poll is not MISSING and msg: + poll._update(msg) + return msg async def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> WebhookMessage: diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 198cdf53ba40..cf23e977b33a 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -61,6 +61,7 @@ from ..file import File from ..embeds import Embed + from ..poll import Poll from ..mentions import AllowedMentions from ..message import Attachment from ..abc import Snowflake @@ -872,6 +873,7 @@ def send( suppress_embeds: bool = MISSING, silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> SyncWebhookMessage: ... @@ -894,6 +896,7 @@ def send( suppress_embeds: bool = MISSING, silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> None: ... @@ -915,6 +918,7 @@ def send( suppress_embeds: bool = False, silent: bool = False, applied_tags: List[ForumTag] = MISSING, + poll: Poll = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. @@ -979,6 +983,14 @@ def send( in the UI, but will not actually send a notification. .. versionadded:: 2.2 + poll: :class:`Poll` + The poll to send with this message. + + .. warning:: + + When sending a Poll via webhook, you cannot manually end it. + + .. versionadded:: 2.4 Raises -------- @@ -1037,6 +1049,7 @@ def send( previous_allowed_mentions=previous_mentions, flags=flags, applied_tags=applied_tag_ids, + poll=poll, ) as params: adapter: WebhookAdapter = _get_webhook_adapter() thread_id: Optional[int] = None @@ -1054,8 +1067,15 @@ def send( wait=wait, ) + msg = None + if wait: - return self._create_message(data, thread=thread) + msg = self._create_message(data, thread=thread) + + if poll is not MISSING and msg: + poll._update(msg) + + return msg def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> SyncWebhookMessage: """Retrieves a single :class:`~discord.SyncWebhookMessage` owned by this webhook. diff --git a/docs/api.rst b/docs/api.rst index b4285c3c1967..13b49df5b4e4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1047,6 +1047,42 @@ Messages :param payload: The raw event payload data. :type payload: :class:`RawBulkMessageDeleteEvent` +Polls +~~~~~~ + +.. function:: on_poll_vote_add(user, answer) + on_poll_vote_remove(user, answer) + + Called when a :class:`Poll` gains or loses a vote. If the ``user`` or ``message`` + are not cached then this event will not be called. + + This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. + + .. note:: + + If the poll allows multiple answers and the user removes or adds multiple votes, this + event will be called as many times as votes that are added or removed. + + .. versionadded:: 2.4 + + :param user: The user that performed the action. + :type user: Union[:class:`User`, :class:`Member`] + :param answer: The answer the user voted or removed their vote from. + :type answer: :class:`PollAnswer` + +.. function:: on_raw_poll_vote_add(payload) + on_raw_poll_vote_remove(payload) + + Called when a :class:`Poll` gains or loses a vote. Unlike :func:`on_poll_vote_add` and :func:`on_poll_vote_remove` + this is called regardless of the state of the internal user and message cache. + + This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. + + .. versionadded:: 2.4 + + :param payload: The raw event payload data. + :type payload: :class:`RawPollVoteActionEvent` + Reactions ~~~~~~~~~~ @@ -3577,6 +3613,16 @@ of :class:`enum.Enum`. The entitlement owner is a user. +.. class:: PollLayoutType + + Represents how a poll answers are shown + + .. versionadded:: 2.4 + + .. attribute:: default + + The default layout. + .. _discord-api-audit-logs: Audit Log Data @@ -5007,6 +5053,14 @@ RawAppCommandPermissionsUpdateEvent .. autoclass:: RawAppCommandPermissionsUpdateEvent() :members: +RawPollVoteActionEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPollVoteActionEvent + +.. autoclass:: RawPollVoteActionEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ @@ -5288,6 +5342,25 @@ ForumTag .. autoclass:: ForumTag :members: +Poll +~~~~ + +.. attributetable:: Poll + +.. autoclass:: Poll() + :members: + +.. attributetable:: PollAnswer + +.. autoclass:: PollAnswer() + :members: + :inherited-members: + +.. attributetable:: PollMedia + +.. autoclass:: PollMedia() + :members: + Exceptions ------------ From f851d9aa3f48504a4714655eaf5204460dc90639 Mon Sep 17 00:00:00 2001 From: z03h <7235242+z03h@users.noreply.github.com> Date: Fri, 10 May 2024 05:19:08 -0700 Subject: [PATCH 046/354] Add missing poll permissions to permission classmethods --- discord/permissions.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/discord/permissions.py b/discord/permissions.py index 916fa4d2fe30..e21c45b0380e 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -187,7 +187,7 @@ def all(cls) -> Self: permissions set to ``True``. """ # Some of these are 0 because we don't want to set unnecessary bits - return cls(0b0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + return cls(0b0000_0000_0000_0010_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: @@ -235,8 +235,11 @@ def all_channel(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. + + .. versionchanged:: 2.4 + Added :attr:`send_polls`, :attr:`send_voice_messages` permissions. """ - return cls(0b0000_0000_0000_0000_0000_0100_0111_1101_1011_0011_1111_0111_1111_1111_0101_0001) + return cls(0b0000_0000_0000_0010_0100_0100_0111_1101_1011_0011_1111_0111_1111_1111_0101_0001) @classmethod def general(cls) -> Self: @@ -278,8 +281,11 @@ def text(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`send_voice_messages` permission. + + .. versionchanged:: 2.4 + Added :attr:`send_polls` permission. """ - return cls(0b0000_0000_0000_0000_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) + return cls(0b0000_0000_0000_0010_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) @classmethod def voice(cls) -> Self: From e5ae306d1ad3d9177744a29591ee9e816da83d76 Mon Sep 17 00:00:00 2001 From: z03h <7235242+z03h@users.noreply.github.com> Date: Fri, 10 May 2024 08:08:06 -0700 Subject: [PATCH 047/354] Add more missing perms to permission classmethods --- discord/permissions.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/discord/permissions.py b/discord/permissions.py index e21c45b0380e..39b0b1a5e4c8 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -224,6 +224,10 @@ def all_channel(cls) -> Self: - :attr:`ban_members` - :attr:`administrator` - :attr:`create_expressions` + - :attr:`moderate_members` + - :attr:`create_events` + - :attr:`manage_events` + - :attr:`view_creator_monetization_analytics` .. versionchanged:: 1.7 Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_application_commands` permissions. @@ -237,9 +241,10 @@ def all_channel(cls) -> Self: Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. .. versionchanged:: 2.4 - Added :attr:`send_polls`, :attr:`send_voice_messages` permissions. + Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, and + :attr:`use_embedded_activities` permissions. """ - return cls(0b0000_0000_0000_0010_0100_0100_0111_1101_1011_0011_1111_0111_1111_1111_0101_0001) + return cls(0b0000_0000_0000_0010_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) @classmethod def general(cls) -> Self: @@ -254,8 +259,11 @@ def general(cls) -> Self: .. versionchanged:: 2.3 Added :attr:`create_expressions` permission. + + .. versionchanged:: 2.4 + Added :attr:`view_creator_monetization_analytics` permission. """ - return cls(0b0000_0000_0000_0000_0000_1000_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) + return cls(0b0000_0000_0000_0000_0000_1010_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) @classmethod def membership(cls) -> Self: From 041abf8b487038c2935da668405ba8b0686ff2f8 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 10 May 2024 17:08:29 +0200 Subject: [PATCH 048/354] Fix Poll.question docstring --- discord/poll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/poll.py b/discord/poll.py index f9b2d04c53a7..0d8e90366679 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -426,7 +426,7 @@ def __repr__(self) -> str: @property def question(self) -> str: - """:class:`str`: Returns this poll answer question string.""" + """:class:`str`: Returns this poll's question string.""" return self._question_media.text @property From 2f479b2ac6b6e1568889376558337a48fcefa246 Mon Sep 17 00:00:00 2001 From: lmaotrigine <57328245+lmaotrigine@users.noreply.github.com> Date: Wed, 15 May 2024 23:07:40 +0530 Subject: [PATCH 049/354] Document bitrate kwarg in StageChannel.edit --- discord/channel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/channel.py b/discord/channel.py index f60e22c0d91a..55b25a03c4ca 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1702,6 +1702,7 @@ async def edit( *, name: str = ..., nsfw: bool = ..., + bitrate: int = ..., user_limit: int = ..., position: int = ..., sync_permissions: int = ..., @@ -1738,6 +1739,8 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona ---------- name: :class:`str` The new channel's name. + bitrate: :class:`int` + The new channel's bitrate. position: :class:`int` The new channel's position. nsfw: :class:`bool` From 9d979d3df1ada4a9b745894ed6ba3b587cb1b307 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 15 May 2024 19:38:19 +0200 Subject: [PATCH 050/354] Some docs fixes for polls --- discord/interactions.py | 4 ++++ discord/poll.py | 6 +++--- docs/api.rst | 21 +++++++++++++-------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 5702e8b8d7b6..5d9d2a6808b9 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -807,6 +807,10 @@ async def send_message( then it is silently ignored. .. versionadded:: 2.1 + poll: :class:`~discord.Poll` + The poll to send with this message. + + .. versionadded:: 2.4 Raises ------- diff --git a/discord/poll.py b/discord/poll.py index 0d8e90366679..b8f036aec268 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -202,7 +202,7 @@ def vote_count(self) -> int: @property def poll(self) -> Poll: - """:class:`Poll`: Returns the parent poll of this answer""" + """:class:`Poll`: Returns the parent poll of this answer.""" return self._poll def _to_dict(self) -> PollAnswerPayload: @@ -310,7 +310,7 @@ class Poll: The duration of the poll. Duration must be in hours. multiple: :class:`bool` Whether users are allowed to select more than one answer. - Defaultsto ``False``. + Defaults to ``False``. layout_type: :class:`PollLayoutType` The layout type of the poll. Defaults to :attr:`PollLayoutType.default`. """ @@ -431,7 +431,7 @@ def question(self) -> str: @property def answers(self) -> List[PollAnswer]: - """List[:class:`PollAnswer`]: Returns a read-only copy of the answers""" + """List[:class:`PollAnswer`]: Returns a read-only copy of the answers.""" return list(self._answers.values()) @property diff --git a/docs/api.rst b/docs/api.rst index 13b49df5b4e4..e8a00b7e4bda 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3615,7 +3615,7 @@ of :class:`enum.Enum`. .. class:: PollLayoutType - Represents how a poll answers are shown + Represents how a poll answers are shown. .. versionadded:: 2.4 @@ -5077,6 +5077,14 @@ PartialWebhookChannel .. autoclass:: PartialWebhookChannel() :members: +PollAnswer +~~~~~~~~~~ + +.. attributetable:: PollAnswer + +.. autoclass:: PollAnswer() + :members: + .. _discord_api_data: Data Classes @@ -5347,18 +5355,15 @@ Poll .. attributetable:: Poll -.. autoclass:: Poll() +.. autoclass:: Poll :members: -.. attributetable:: PollAnswer - -.. autoclass:: PollAnswer() - :members: - :inherited-members: +PollMedia +~~~~~~~~~ .. attributetable:: PollMedia -.. autoclass:: PollMedia() +.. autoclass:: PollMedia :members: From 2751b55357d9327ef068f0396beaefa19b4295cd Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 16 May 2024 02:45:06 +0200 Subject: [PATCH 051/354] Fix Webhook poll sending raising AttributeError with a mocked state --- discord/webhook/async_.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index d04e21b57d55..3b0416f9a72c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -721,6 +721,11 @@ def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]: return self._parent._get_guild(guild_id) return None + def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: + if self._parent is not None: + return self._parent._get_poll(msg_id) + return None + def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser: if self._parent is not None: return self._parent.store_user(data, cache=cache) From d53877f2cb424021f32a925e37787e4314071388 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 16 May 2024 18:48:21 -0400 Subject: [PATCH 052/354] Remove async_timeout requirement It was causing some dependency issues --- discord/voice_state.py | 62 ++++++++++++++++++++---------------------- requirements.txt | 1 - 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/discord/voice_state.py b/discord/voice_state.py index f4a5f76b37ba..03ce93ae1fca 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -45,11 +45,6 @@ import logging import threading -try: - from asyncio import timeout as atimeout # type: ignore -except ImportError: - from async_timeout import timeout as atimeout # type: ignore - from typing import TYPE_CHECKING, Optional, Dict, List, Callable, Coroutine, Any, Tuple from .enums import Enum @@ -400,37 +395,41 @@ async def _wrap_connect(self, *args: Any) -> None: await self.disconnect() raise - async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: - _log.info('Connecting to voice...') + async def _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: + for i in range(5): + _log.info('Starting voice handshake... (connection attempt %d)', i + 1) - async with atimeout(timeout): - for i in range(5): - _log.info('Starting voice handshake... (connection attempt %d)', i + 1) + await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) + # Setting this unnecessarily will break reconnecting + if self.state is ConnectionFlowState.disconnected: + self.state = ConnectionFlowState.set_guild_voice_state - await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) - # Setting this unnecessarily will break reconnecting - if self.state is ConnectionFlowState.disconnected: - self.state = ConnectionFlowState.set_guild_voice_state + await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) - await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) + _log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) - _log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) + try: + self.ws = await self._connect_websocket(resume) + await self._handshake_websocket() + break + except ConnectionClosed: + if reconnect: + wait = 1 + i * 2.0 + _log.exception('Failed to connect to voice... Retrying in %ss...', wait) + await self.disconnect(cleanup=False) + await asyncio.sleep(wait) + continue + else: + await self.disconnect() + raise - try: - self.ws = await self._connect_websocket(resume) - await self._handshake_websocket() - break - except ConnectionClosed: - if reconnect: - wait = 1 + i * 2.0 - _log.exception('Failed to connect to voice... Retrying in %ss...', wait) - await self.disconnect(cleanup=False) - await asyncio.sleep(wait) - continue - else: - await self.disconnect() - raise + async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: + _log.info('Connecting to voice...') + await asyncio.wait_for( + self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), + timeout=timeout, + ) _log.info('Voice connection complete.') if not self._runner: @@ -472,8 +471,7 @@ async def disconnect(self, *, force: bool = True, cleanup: bool = True, wait: bo # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still # connecting leaving it in a bad state. Since there's no nice way to transfer state to the new one, we have to do this. try: - async with atimeout(self.timeout): - await self._disconnected.wait() + await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) except TimeoutError: _log.debug('Timed out waiting for voice disconnection confirmation') except asyncio.CancelledError: diff --git a/requirements.txt b/requirements.txt index 74dedab377f8..046084ebb6d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ aiohttp>=3.7.4,<4 -async-timeout>=4.0,<5.0; python_version<"3.11" From b0024dc86652db19cad16296d6c16c6272b2eaa3 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 16 May 2024 18:48:48 -0400 Subject: [PATCH 053/354] Change line endings from CRLF to LF in voice_state.py --- discord/voice_state.py | 1376 ++++++++++++++++++++-------------------- 1 file changed, 688 insertions(+), 688 deletions(-) diff --git a/discord/voice_state.py b/discord/voice_state.py index 03ce93ae1fca..f10a307d6715 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -1,688 +1,688 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-present Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - - -Some documentation to refer to: - -- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID. -- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE. -- We pull the session_id from VOICE_STATE_UPDATE. -- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE. -- Then we initiate the voice web socket (vWS) pointing to the endpoint. -- We send opcode 0 with the user_id, server_id, session_id and token using the vWS. -- The vWS sends back opcode 2 with an ssrc, port, modes(array) and heartbeat_interval. -- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE. -- Then we send our IP and port via vWS with opcode 1. -- When that's all done, we receive opcode 4 from the vWS. -- Finally we can transmit data to endpoint:port. -""" - -from __future__ import annotations - -import select -import socket -import asyncio -import logging -import threading - -from typing import TYPE_CHECKING, Optional, Dict, List, Callable, Coroutine, Any, Tuple - -from .enums import Enum -from .utils import MISSING, sane_wait_for -from .errors import ConnectionClosed -from .backoff import ExponentialBackoff -from .gateway import DiscordVoiceWebSocket - -if TYPE_CHECKING: - from . import abc - from .guild import Guild - from .user import ClientUser - from .member import VoiceState - from .voice_client import VoiceClient - - from .types.voice import ( - GuildVoiceState as GuildVoiceStatePayload, - VoiceServerUpdate as VoiceServerUpdatePayload, - SupportedModes, - ) - - WebsocketHook = Optional[Callable[[DiscordVoiceWebSocket, Dict[str, Any]], Coroutine[Any, Any, Any]]] - SocketReaderCallback = Callable[[bytes], Any] - - -__all__ = ('VoiceConnectionState',) - -_log = logging.getLogger(__name__) - - -class SocketReader(threading.Thread): - def __init__(self, state: VoiceConnectionState, *, start_paused: bool = True) -> None: - super().__init__(daemon=True, name=f'voice-socket-reader:{id(self):#x}') - self.state: VoiceConnectionState = state - self.start_paused = start_paused - self._callbacks: List[SocketReaderCallback] = [] - self._running = threading.Event() - self._end = threading.Event() - # If we have paused reading due to having no callbacks - self._idle_paused: bool = True - - def register(self, callback: SocketReaderCallback) -> None: - self._callbacks.append(callback) - if self._idle_paused: - self._idle_paused = False - self._running.set() - - def unregister(self, callback: SocketReaderCallback) -> None: - try: - self._callbacks.remove(callback) - except ValueError: - pass - else: - if not self._callbacks and self._running.is_set(): - # If running is not set, we are either explicitly paused and - # should be explicitly resumed, or we are already idle paused - self._idle_paused = True - self._running.clear() - - def pause(self) -> None: - self._idle_paused = False - self._running.clear() - - def resume(self, *, force: bool = False) -> None: - if self._running.is_set(): - return - # Don't resume if there are no callbacks registered - if not force and not self._callbacks: - # We tried to resume but there was nothing to do, so resume when ready - self._idle_paused = True - return - self._idle_paused = False - self._running.set() - - def stop(self) -> None: - self._end.set() - self._running.set() - - def run(self) -> None: - self._end.clear() - self._running.set() - if self.start_paused: - self.pause() - try: - self._do_run() - except Exception: - _log.exception('Error in %s', self) - finally: - self.stop() - self._running.clear() - self._callbacks.clear() - - def _do_run(self) -> None: - while not self._end.is_set(): - if not self._running.is_set(): - self._running.wait() - continue - - # Since this socket is a non blocking socket, select has to be used to wait on it for reading. - try: - readable, _, _ = select.select([self.state.socket], [], [], 30) - except (ValueError, TypeError, OSError) as e: - _log.debug( - "Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e - ) - # The socket is either closed or doesn't exist at the moment - continue - - if not readable: - continue - - try: - data = self.state.socket.recv(2048) - except OSError: - _log.debug('Error reading from socket in %s, this should be safe to ignore', self, exc_info=True) - else: - for cb in self._callbacks: - try: - cb(data) - except Exception: - _log.exception('Error calling %s in %s', cb, self) - - -class ConnectionFlowState(Enum): - """Enum representing voice connection flow state.""" - - # fmt: off - disconnected = 0 - set_guild_voice_state = 1 - got_voice_state_update = 2 - got_voice_server_update = 3 - got_both_voice_updates = 4 - websocket_connected = 5 - got_websocket_ready = 6 - got_ip_discovery = 7 - connected = 8 - # fmt: on - - -class VoiceConnectionState: - """Represents the internal state of a voice connection.""" - - def __init__(self, voice_client: VoiceClient, *, hook: Optional[WebsocketHook] = None) -> None: - self.voice_client = voice_client - self.hook = hook - - self.timeout: float = 30.0 - self.reconnect: bool = True - self.self_deaf: bool = False - self.self_mute: bool = False - self.token: Optional[str] = None - self.session_id: Optional[str] = None - self.endpoint: Optional[str] = None - self.endpoint_ip: Optional[str] = None - self.server_id: Optional[int] = None - self.ip: Optional[str] = None - self.port: Optional[int] = None - self.voice_port: Optional[int] = None - self.secret_key: List[int] = MISSING - self.ssrc: int = MISSING - self.mode: SupportedModes = MISSING - self.socket: socket.socket = MISSING - self.ws: DiscordVoiceWebSocket = MISSING - - self._state: ConnectionFlowState = ConnectionFlowState.disconnected - self._expecting_disconnect: bool = False - self._connected = threading.Event() - self._state_event = asyncio.Event() - self._disconnected = asyncio.Event() - self._runner: Optional[asyncio.Task] = None - self._connector: Optional[asyncio.Task] = None - self._socket_reader = SocketReader(self) - self._socket_reader.start() - - @property - def state(self) -> ConnectionFlowState: - return self._state - - @state.setter - def state(self, state: ConnectionFlowState) -> None: - if state is not self._state: - _log.debug('Connection state changed to %s', state.name) - self._state = state - self._state_event.set() - self._state_event.clear() - - if state is ConnectionFlowState.connected: - self._connected.set() - else: - self._connected.clear() - - @property - def guild(self) -> Guild: - return self.voice_client.guild - - @property - def user(self) -> ClientUser: - return self.voice_client.user - - @property - def supported_modes(self) -> Tuple[SupportedModes, ...]: - return self.voice_client.supported_modes - - @property - def self_voice_state(self) -> Optional[VoiceState]: - return self.guild.me.voice - - async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: - channel_id = data['channel_id'] - - if channel_id is None: - self._disconnected.set() - - # If we know we're going to get a voice_state_update where we have no channel due to - # being in the reconnect or disconnect flow, we ignore it. Otherwise, it probably wasn't from us. - if self._expecting_disconnect: - self._expecting_disconnect = False - else: - _log.debug('We were externally disconnected from voice.') - await self.disconnect() - - return - - channel_id = int(channel_id) - self.session_id = data['session_id'] - - # we got the event while connecting - if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update): - if self.state is ConnectionFlowState.set_guild_voice_state: - self.state = ConnectionFlowState.got_voice_state_update - - # we moved ourselves - if channel_id != self.voice_client.channel.id: - self._update_voice_channel(channel_id) - - else: - self.state = ConnectionFlowState.got_both_voice_updates - return - - if self.state is ConnectionFlowState.connected: - self._update_voice_channel(channel_id) - - elif self.state is not ConnectionFlowState.disconnected: - if channel_id != self.voice_client.channel.id: - # For some unfortunate reason we were moved during the connection flow - _log.info('Handling channel move while connecting...') - - self._update_voice_channel(channel_id) - await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update) - await self.connect( - reconnect=self.reconnect, - timeout=self.timeout, - self_deaf=(self.self_voice_state or self).self_deaf, - self_mute=(self.self_voice_state or self).self_mute, - resume=False, - wait=False, - ) - else: - _log.debug('Ignoring unexpected voice_state_update event') - - async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: - previous_token = self.token - previous_server_id = self.server_id - previous_endpoint = self.endpoint - - self.token = data['token'] - self.server_id = int(data['guild_id']) - endpoint = data.get('endpoint') - - if self.token is None or endpoint is None: - _log.warning( - 'Awaiting endpoint... This requires waiting. ' - 'If timeout occurred considering raising the timeout and reconnecting.' - ) - return - - self.endpoint, _, _ = endpoint.rpartition(':') - if self.endpoint.startswith('wss://'): - # Just in case, strip it off since we're going to add it later - self.endpoint = self.endpoint[6:] - - # we got the event while connecting - if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_state_update): - # This gets set after READY is received - self.endpoint_ip = MISSING - self._create_socket() - - if self.state is ConnectionFlowState.set_guild_voice_state: - self.state = ConnectionFlowState.got_voice_server_update - else: - self.state = ConnectionFlowState.got_both_voice_updates - - elif self.state is ConnectionFlowState.connected: - _log.debug('Voice server update, closing old voice websocket') - await self.ws.close(4014) - self.state = ConnectionFlowState.got_voice_server_update - - elif self.state is not ConnectionFlowState.disconnected: - # eventual consistency - if previous_token == self.token and previous_server_id == self.server_id and previous_token == self.token: - return - - _log.debug('Unexpected server update event, attempting to handle') - - await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update) - await self.connect( - reconnect=self.reconnect, - timeout=self.timeout, - self_deaf=(self.self_voice_state or self).self_deaf, - self_mute=(self.self_voice_state or self).self_mute, - resume=False, - wait=False, - ) - self._create_socket() - - async def connect( - self, *, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool, wait: bool = True - ) -> None: - if self._connector: - self._connector.cancel() - self._connector = None - - if self._runner: - self._runner.cancel() - self._runner = None - - self.timeout = timeout - self.reconnect = reconnect - self._connector = self.voice_client.loop.create_task( - self._wrap_connect(reconnect, timeout, self_deaf, self_mute, resume), name='Voice connector' - ) - if wait: - await self._connector - - async def _wrap_connect(self, *args: Any) -> None: - try: - await self._connect(*args) - except asyncio.CancelledError: - _log.debug('Cancelling voice connection') - await self.soft_disconnect() - raise - except asyncio.TimeoutError: - _log.info('Timed out connecting to voice') - await self.disconnect() - raise - except Exception: - _log.exception('Error connecting to voice... disconnecting') - await self.disconnect() - raise - - async def _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: - for i in range(5): - _log.info('Starting voice handshake... (connection attempt %d)', i + 1) - - await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) - # Setting this unnecessarily will break reconnecting - if self.state is ConnectionFlowState.disconnected: - self.state = ConnectionFlowState.set_guild_voice_state - - await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) - - _log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) - - try: - self.ws = await self._connect_websocket(resume) - await self._handshake_websocket() - break - except ConnectionClosed: - if reconnect: - wait = 1 + i * 2.0 - _log.exception('Failed to connect to voice... Retrying in %ss...', wait) - await self.disconnect(cleanup=False) - await asyncio.sleep(wait) - continue - else: - await self.disconnect() - raise - - async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: - _log.info('Connecting to voice...') - - await asyncio.wait_for( - self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), - timeout=timeout, - ) - _log.info('Voice connection complete.') - - if not self._runner: - self._runner = self.voice_client.loop.create_task(self._poll_voice_ws(reconnect), name='Voice websocket poller') - - async def disconnect(self, *, force: bool = True, cleanup: bool = True, wait: bool = False) -> None: - if not force and not self.is_connected(): - return - - try: - await self._voice_disconnect() - if self.ws: - await self.ws.close() - except Exception: - _log.debug('Ignoring exception disconnecting from voice', exc_info=True) - finally: - self.state = ConnectionFlowState.disconnected - self._socket_reader.pause() - - # Stop threads before we unlock waiters so they end properly - if cleanup: - self._socket_reader.stop() - self.voice_client.stop() - - # Flip the connected event to unlock any waiters - self._connected.set() - self._connected.clear() - - if self.socket: - self.socket.close() - - self.ip = MISSING - self.port = MISSING - - # Skip this part if disconnect was called from the poll loop task - if wait and not self._inside_runner(): - # Wait for the voice_state_update event confirming the bot left the voice channel. - # This prevents a race condition caused by disconnecting and immediately connecting again. - # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still - # connecting leaving it in a bad state. Since there's no nice way to transfer state to the new one, we have to do this. - try: - await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) - except TimeoutError: - _log.debug('Timed out waiting for voice disconnection confirmation') - except asyncio.CancelledError: - pass - - if cleanup: - self.voice_client.cleanup() - - async def soft_disconnect(self, *, with_state: ConnectionFlowState = ConnectionFlowState.got_both_voice_updates) -> None: - _log.debug('Soft disconnecting from voice') - # Stop the websocket reader because closing the websocket will trigger an unwanted reconnect - if self._runner: - self._runner.cancel() - self._runner = None - - try: - if self.ws: - await self.ws.close() - except Exception: - _log.debug('Ignoring exception soft disconnecting from voice', exc_info=True) - finally: - self.state = with_state - self._socket_reader.pause() - - if self.socket: - self.socket.close() - - self.ip = MISSING - self.port = MISSING - - async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[float]) -> None: - if channel is None: - # This function should only be called externally so its ok to wait for the disconnect. - await self.disconnect(wait=True) - return - - if self.voice_client.channel and channel.id == self.voice_client.channel.id: - return - - previous_state = self.state - - # this is only an outgoing ws request - # if it fails, nothing happens and nothing changes (besides self.state) - await self._move_to(channel) - - last_state = self.state - try: - await self.wait_async(timeout) - except asyncio.TimeoutError: - _log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id) - if self.state is last_state: - _log.debug('Reverting to previous state %s', previous_state.name) - self.state = previous_state - - def wait(self, timeout: Optional[float] = None) -> bool: - return self._connected.wait(timeout) - - async def wait_async(self, timeout: Optional[float] = None) -> None: - await self._wait_for_state(ConnectionFlowState.connected, timeout=timeout) - - def is_connected(self) -> bool: - return self.state is ConnectionFlowState.connected - - def send_packet(self, packet: bytes) -> None: - self.socket.sendall(packet) - - def add_socket_listener(self, callback: SocketReaderCallback) -> None: - _log.debug('Registering socket listener callback %s', callback) - self._socket_reader.register(callback) - - def remove_socket_listener(self, callback: SocketReaderCallback) -> None: - _log.debug('Unregistering socket listener callback %s', callback) - self._socket_reader.unregister(callback) - - def _inside_runner(self) -> bool: - return self._runner is not None and asyncio.current_task() == self._runner - - async def _wait_for_state( - self, state: ConnectionFlowState, *other_states: ConnectionFlowState, timeout: Optional[float] = None - ) -> None: - states = (state, *other_states) - while True: - if self.state in states: - return - await sane_wait_for([self._state_event.wait()], timeout=timeout) - - async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False) -> None: - channel = self.voice_client.channel - await channel.guild.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) - - async def _voice_disconnect(self) -> None: - _log.info( - 'The voice handshake is being terminated for Channel ID %s (Guild ID %s)', - self.voice_client.channel.id, - self.voice_client.guild.id, - ) - self.state = ConnectionFlowState.disconnected - await self.voice_client.channel.guild.change_voice_state(channel=None) - self._expecting_disconnect = True - self._disconnected.clear() - - async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: - ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook) - self.state = ConnectionFlowState.websocket_connected - return ws - - async def _handshake_websocket(self) -> None: - while not self.ip: - await self.ws.poll_event() - self.state = ConnectionFlowState.got_ip_discovery - while self.ws.secret_key is None: - await self.ws.poll_event() - self.state = ConnectionFlowState.connected - - def _create_socket(self) -> None: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.socket.setblocking(False) - self._socket_reader.resume() - - async def _poll_voice_ws(self, reconnect: bool) -> None: - backoff = ExponentialBackoff() - while True: - try: - await self.ws.poll_event() - except asyncio.CancelledError: - return - except (ConnectionClosed, asyncio.TimeoutError) as exc: - if isinstance(exc, ConnectionClosed): - # The following close codes are undocumented so I will document them here. - # 1000 - normal closure (obviously) - # 4014 - we were externally disconnected (voice channel deleted, we were moved, etc) - # 4015 - voice server has crashed - if exc.code in (1000, 4015): - # Don't call disconnect a second time if the websocket closed from a disconnect call - if not self._expecting_disconnect: - _log.info('Disconnecting from voice normally, close code %d.', exc.code) - await self.disconnect() - break - - if exc.code == 4014: - # We were disconnected by discord - # This condition is a race between the main ws event and the voice ws closing - if self._disconnected.is_set(): - _log.info('Disconnected from voice by discord, close code %d.', exc.code) - await self.disconnect() - break - - # We may have been moved to a different channel - _log.info('Disconnected from voice by force... potentially reconnecting.') - successful = await self._potential_reconnect() - if not successful: - _log.info('Reconnect was unsuccessful, disconnecting from voice normally...') - # Don't bother to disconnect if already disconnected - if self.state is not ConnectionFlowState.disconnected: - await self.disconnect() - break - else: - continue - - _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') - - if not reconnect: - await self.disconnect() - raise - - retry = backoff.delay() - _log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) - await asyncio.sleep(retry) - await self.disconnect(cleanup=False) - - try: - await self._connect( - reconnect=reconnect, - timeout=self.timeout, - self_deaf=(self.self_voice_state or self).self_deaf, - self_mute=(self.self_voice_state or self).self_mute, - resume=False, - ) - except asyncio.TimeoutError: - # at this point we've retried 5 times... let's continue the loop. - _log.warning('Could not connect to voice... Retrying...') - continue - - async def _potential_reconnect(self) -> bool: - try: - await self._wait_for_state( - ConnectionFlowState.got_voice_server_update, - ConnectionFlowState.got_both_voice_updates, - ConnectionFlowState.disconnected, - timeout=self.timeout, - ) - except asyncio.TimeoutError: - return False - else: - if self.state is ConnectionFlowState.disconnected: - return False - - previous_ws = self.ws - try: - self.ws = await self._connect_websocket(False) - await self._handshake_websocket() - except (ConnectionClosed, asyncio.TimeoutError): - return False - else: - return True - finally: - await previous_ws.close() - - async def _move_to(self, channel: abc.Snowflake) -> None: - await self.voice_client.channel.guild.change_voice_state(channel=channel) - self.state = ConnectionFlowState.set_guild_voice_state - - def _update_voice_channel(self, channel_id: Optional[int]) -> None: - self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +Some documentation to refer to: + +- Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID. +- The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE. +- We pull the session_id from VOICE_STATE_UPDATE. +- We pull the token, endpoint and server_id from VOICE_SERVER_UPDATE. +- Then we initiate the voice web socket (vWS) pointing to the endpoint. +- We send opcode 0 with the user_id, server_id, session_id and token using the vWS. +- The vWS sends back opcode 2 with an ssrc, port, modes(array) and heartbeat_interval. +- We send a UDP discovery packet to endpoint:port and receive our IP and our port in LE. +- Then we send our IP and port via vWS with opcode 1. +- When that's all done, we receive opcode 4 from the vWS. +- Finally we can transmit data to endpoint:port. +""" + +from __future__ import annotations + +import select +import socket +import asyncio +import logging +import threading + +from typing import TYPE_CHECKING, Optional, Dict, List, Callable, Coroutine, Any, Tuple + +from .enums import Enum +from .utils import MISSING, sane_wait_for +from .errors import ConnectionClosed +from .backoff import ExponentialBackoff +from .gateway import DiscordVoiceWebSocket + +if TYPE_CHECKING: + from . import abc + from .guild import Guild + from .user import ClientUser + from .member import VoiceState + from .voice_client import VoiceClient + + from .types.voice import ( + GuildVoiceState as GuildVoiceStatePayload, + VoiceServerUpdate as VoiceServerUpdatePayload, + SupportedModes, + ) + + WebsocketHook = Optional[Callable[[DiscordVoiceWebSocket, Dict[str, Any]], Coroutine[Any, Any, Any]]] + SocketReaderCallback = Callable[[bytes], Any] + + +__all__ = ('VoiceConnectionState',) + +_log = logging.getLogger(__name__) + + +class SocketReader(threading.Thread): + def __init__(self, state: VoiceConnectionState, *, start_paused: bool = True) -> None: + super().__init__(daemon=True, name=f'voice-socket-reader:{id(self):#x}') + self.state: VoiceConnectionState = state + self.start_paused = start_paused + self._callbacks: List[SocketReaderCallback] = [] + self._running = threading.Event() + self._end = threading.Event() + # If we have paused reading due to having no callbacks + self._idle_paused: bool = True + + def register(self, callback: SocketReaderCallback) -> None: + self._callbacks.append(callback) + if self._idle_paused: + self._idle_paused = False + self._running.set() + + def unregister(self, callback: SocketReaderCallback) -> None: + try: + self._callbacks.remove(callback) + except ValueError: + pass + else: + if not self._callbacks and self._running.is_set(): + # If running is not set, we are either explicitly paused and + # should be explicitly resumed, or we are already idle paused + self._idle_paused = True + self._running.clear() + + def pause(self) -> None: + self._idle_paused = False + self._running.clear() + + def resume(self, *, force: bool = False) -> None: + if self._running.is_set(): + return + # Don't resume if there are no callbacks registered + if not force and not self._callbacks: + # We tried to resume but there was nothing to do, so resume when ready + self._idle_paused = True + return + self._idle_paused = False + self._running.set() + + def stop(self) -> None: + self._end.set() + self._running.set() + + def run(self) -> None: + self._end.clear() + self._running.set() + if self.start_paused: + self.pause() + try: + self._do_run() + except Exception: + _log.exception('Error in %s', self) + finally: + self.stop() + self._running.clear() + self._callbacks.clear() + + def _do_run(self) -> None: + while not self._end.is_set(): + if not self._running.is_set(): + self._running.wait() + continue + + # Since this socket is a non blocking socket, select has to be used to wait on it for reading. + try: + readable, _, _ = select.select([self.state.socket], [], [], 30) + except (ValueError, TypeError, OSError) as e: + _log.debug( + "Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e + ) + # The socket is either closed or doesn't exist at the moment + continue + + if not readable: + continue + + try: + data = self.state.socket.recv(2048) + except OSError: + _log.debug('Error reading from socket in %s, this should be safe to ignore', self, exc_info=True) + else: + for cb in self._callbacks: + try: + cb(data) + except Exception: + _log.exception('Error calling %s in %s', cb, self) + + +class ConnectionFlowState(Enum): + """Enum representing voice connection flow state.""" + + # fmt: off + disconnected = 0 + set_guild_voice_state = 1 + got_voice_state_update = 2 + got_voice_server_update = 3 + got_both_voice_updates = 4 + websocket_connected = 5 + got_websocket_ready = 6 + got_ip_discovery = 7 + connected = 8 + # fmt: on + + +class VoiceConnectionState: + """Represents the internal state of a voice connection.""" + + def __init__(self, voice_client: VoiceClient, *, hook: Optional[WebsocketHook] = None) -> None: + self.voice_client = voice_client + self.hook = hook + + self.timeout: float = 30.0 + self.reconnect: bool = True + self.self_deaf: bool = False + self.self_mute: bool = False + self.token: Optional[str] = None + self.session_id: Optional[str] = None + self.endpoint: Optional[str] = None + self.endpoint_ip: Optional[str] = None + self.server_id: Optional[int] = None + self.ip: Optional[str] = None + self.port: Optional[int] = None + self.voice_port: Optional[int] = None + self.secret_key: List[int] = MISSING + self.ssrc: int = MISSING + self.mode: SupportedModes = MISSING + self.socket: socket.socket = MISSING + self.ws: DiscordVoiceWebSocket = MISSING + + self._state: ConnectionFlowState = ConnectionFlowState.disconnected + self._expecting_disconnect: bool = False + self._connected = threading.Event() + self._state_event = asyncio.Event() + self._disconnected = asyncio.Event() + self._runner: Optional[asyncio.Task] = None + self._connector: Optional[asyncio.Task] = None + self._socket_reader = SocketReader(self) + self._socket_reader.start() + + @property + def state(self) -> ConnectionFlowState: + return self._state + + @state.setter + def state(self, state: ConnectionFlowState) -> None: + if state is not self._state: + _log.debug('Connection state changed to %s', state.name) + self._state = state + self._state_event.set() + self._state_event.clear() + + if state is ConnectionFlowState.connected: + self._connected.set() + else: + self._connected.clear() + + @property + def guild(self) -> Guild: + return self.voice_client.guild + + @property + def user(self) -> ClientUser: + return self.voice_client.user + + @property + def supported_modes(self) -> Tuple[SupportedModes, ...]: + return self.voice_client.supported_modes + + @property + def self_voice_state(self) -> Optional[VoiceState]: + return self.guild.me.voice + + async def voice_state_update(self, data: GuildVoiceStatePayload) -> None: + channel_id = data['channel_id'] + + if channel_id is None: + self._disconnected.set() + + # If we know we're going to get a voice_state_update where we have no channel due to + # being in the reconnect or disconnect flow, we ignore it. Otherwise, it probably wasn't from us. + if self._expecting_disconnect: + self._expecting_disconnect = False + else: + _log.debug('We were externally disconnected from voice.') + await self.disconnect() + + return + + channel_id = int(channel_id) + self.session_id = data['session_id'] + + # we got the event while connecting + if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_server_update): + if self.state is ConnectionFlowState.set_guild_voice_state: + self.state = ConnectionFlowState.got_voice_state_update + + # we moved ourselves + if channel_id != self.voice_client.channel.id: + self._update_voice_channel(channel_id) + + else: + self.state = ConnectionFlowState.got_both_voice_updates + return + + if self.state is ConnectionFlowState.connected: + self._update_voice_channel(channel_id) + + elif self.state is not ConnectionFlowState.disconnected: + if channel_id != self.voice_client.channel.id: + # For some unfortunate reason we were moved during the connection flow + _log.info('Handling channel move while connecting...') + + self._update_voice_channel(channel_id) + await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_state_update) + await self.connect( + reconnect=self.reconnect, + timeout=self.timeout, + self_deaf=(self.self_voice_state or self).self_deaf, + self_mute=(self.self_voice_state or self).self_mute, + resume=False, + wait=False, + ) + else: + _log.debug('Ignoring unexpected voice_state_update event') + + async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: + previous_token = self.token + previous_server_id = self.server_id + previous_endpoint = self.endpoint + + self.token = data['token'] + self.server_id = int(data['guild_id']) + endpoint = data.get('endpoint') + + if self.token is None or endpoint is None: + _log.warning( + 'Awaiting endpoint... This requires waiting. ' + 'If timeout occurred considering raising the timeout and reconnecting.' + ) + return + + self.endpoint, _, _ = endpoint.rpartition(':') + if self.endpoint.startswith('wss://'): + # Just in case, strip it off since we're going to add it later + self.endpoint = self.endpoint[6:] + + # we got the event while connecting + if self.state in (ConnectionFlowState.set_guild_voice_state, ConnectionFlowState.got_voice_state_update): + # This gets set after READY is received + self.endpoint_ip = MISSING + self._create_socket() + + if self.state is ConnectionFlowState.set_guild_voice_state: + self.state = ConnectionFlowState.got_voice_server_update + else: + self.state = ConnectionFlowState.got_both_voice_updates + + elif self.state is ConnectionFlowState.connected: + _log.debug('Voice server update, closing old voice websocket') + await self.ws.close(4014) + self.state = ConnectionFlowState.got_voice_server_update + + elif self.state is not ConnectionFlowState.disconnected: + # eventual consistency + if previous_token == self.token and previous_server_id == self.server_id and previous_token == self.token: + return + + _log.debug('Unexpected server update event, attempting to handle') + + await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update) + await self.connect( + reconnect=self.reconnect, + timeout=self.timeout, + self_deaf=(self.self_voice_state or self).self_deaf, + self_mute=(self.self_voice_state or self).self_mute, + resume=False, + wait=False, + ) + self._create_socket() + + async def connect( + self, *, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool, wait: bool = True + ) -> None: + if self._connector: + self._connector.cancel() + self._connector = None + + if self._runner: + self._runner.cancel() + self._runner = None + + self.timeout = timeout + self.reconnect = reconnect + self._connector = self.voice_client.loop.create_task( + self._wrap_connect(reconnect, timeout, self_deaf, self_mute, resume), name='Voice connector' + ) + if wait: + await self._connector + + async def _wrap_connect(self, *args: Any) -> None: + try: + await self._connect(*args) + except asyncio.CancelledError: + _log.debug('Cancelling voice connection') + await self.soft_disconnect() + raise + except asyncio.TimeoutError: + _log.info('Timed out connecting to voice') + await self.disconnect() + raise + except Exception: + _log.exception('Error connecting to voice... disconnecting') + await self.disconnect() + raise + + async def _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: + for i in range(5): + _log.info('Starting voice handshake... (connection attempt %d)', i + 1) + + await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) + # Setting this unnecessarily will break reconnecting + if self.state is ConnectionFlowState.disconnected: + self.state = ConnectionFlowState.set_guild_voice_state + + await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) + + _log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) + + try: + self.ws = await self._connect_websocket(resume) + await self._handshake_websocket() + break + except ConnectionClosed: + if reconnect: + wait = 1 + i * 2.0 + _log.exception('Failed to connect to voice... Retrying in %ss...', wait) + await self.disconnect(cleanup=False) + await asyncio.sleep(wait) + continue + else: + await self.disconnect() + raise + + async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: + _log.info('Connecting to voice...') + + await asyncio.wait_for( + self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), + timeout=timeout, + ) + _log.info('Voice connection complete.') + + if not self._runner: + self._runner = self.voice_client.loop.create_task(self._poll_voice_ws(reconnect), name='Voice websocket poller') + + async def disconnect(self, *, force: bool = True, cleanup: bool = True, wait: bool = False) -> None: + if not force and not self.is_connected(): + return + + try: + await self._voice_disconnect() + if self.ws: + await self.ws.close() + except Exception: + _log.debug('Ignoring exception disconnecting from voice', exc_info=True) + finally: + self.state = ConnectionFlowState.disconnected + self._socket_reader.pause() + + # Stop threads before we unlock waiters so they end properly + if cleanup: + self._socket_reader.stop() + self.voice_client.stop() + + # Flip the connected event to unlock any waiters + self._connected.set() + self._connected.clear() + + if self.socket: + self.socket.close() + + self.ip = MISSING + self.port = MISSING + + # Skip this part if disconnect was called from the poll loop task + if wait and not self._inside_runner(): + # Wait for the voice_state_update event confirming the bot left the voice channel. + # This prevents a race condition caused by disconnecting and immediately connecting again. + # The new VoiceConnectionState object receives the voice_state_update event containing channel=None while still + # connecting leaving it in a bad state. Since there's no nice way to transfer state to the new one, we have to do this. + try: + await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) + except TimeoutError: + _log.debug('Timed out waiting for voice disconnection confirmation') + except asyncio.CancelledError: + pass + + if cleanup: + self.voice_client.cleanup() + + async def soft_disconnect(self, *, with_state: ConnectionFlowState = ConnectionFlowState.got_both_voice_updates) -> None: + _log.debug('Soft disconnecting from voice') + # Stop the websocket reader because closing the websocket will trigger an unwanted reconnect + if self._runner: + self._runner.cancel() + self._runner = None + + try: + if self.ws: + await self.ws.close() + except Exception: + _log.debug('Ignoring exception soft disconnecting from voice', exc_info=True) + finally: + self.state = with_state + self._socket_reader.pause() + + if self.socket: + self.socket.close() + + self.ip = MISSING + self.port = MISSING + + async def move_to(self, channel: Optional[abc.Snowflake], timeout: Optional[float]) -> None: + if channel is None: + # This function should only be called externally so its ok to wait for the disconnect. + await self.disconnect(wait=True) + return + + if self.voice_client.channel and channel.id == self.voice_client.channel.id: + return + + previous_state = self.state + + # this is only an outgoing ws request + # if it fails, nothing happens and nothing changes (besides self.state) + await self._move_to(channel) + + last_state = self.state + try: + await self.wait_async(timeout) + except asyncio.TimeoutError: + _log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id) + if self.state is last_state: + _log.debug('Reverting to previous state %s', previous_state.name) + self.state = previous_state + + def wait(self, timeout: Optional[float] = None) -> bool: + return self._connected.wait(timeout) + + async def wait_async(self, timeout: Optional[float] = None) -> None: + await self._wait_for_state(ConnectionFlowState.connected, timeout=timeout) + + def is_connected(self) -> bool: + return self.state is ConnectionFlowState.connected + + def send_packet(self, packet: bytes) -> None: + self.socket.sendall(packet) + + def add_socket_listener(self, callback: SocketReaderCallback) -> None: + _log.debug('Registering socket listener callback %s', callback) + self._socket_reader.register(callback) + + def remove_socket_listener(self, callback: SocketReaderCallback) -> None: + _log.debug('Unregistering socket listener callback %s', callback) + self._socket_reader.unregister(callback) + + def _inside_runner(self) -> bool: + return self._runner is not None and asyncio.current_task() == self._runner + + async def _wait_for_state( + self, state: ConnectionFlowState, *other_states: ConnectionFlowState, timeout: Optional[float] = None + ) -> None: + states = (state, *other_states) + while True: + if self.state in states: + return + await sane_wait_for([self._state_event.wait()], timeout=timeout) + + async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False) -> None: + channel = self.voice_client.channel + await channel.guild.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) + + async def _voice_disconnect(self) -> None: + _log.info( + 'The voice handshake is being terminated for Channel ID %s (Guild ID %s)', + self.voice_client.channel.id, + self.voice_client.guild.id, + ) + self.state = ConnectionFlowState.disconnected + await self.voice_client.channel.guild.change_voice_state(channel=None) + self._expecting_disconnect = True + self._disconnected.clear() + + async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: + ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook) + self.state = ConnectionFlowState.websocket_connected + return ws + + async def _handshake_websocket(self) -> None: + while not self.ip: + await self.ws.poll_event() + self.state = ConnectionFlowState.got_ip_discovery + while self.ws.secret_key is None: + await self.ws.poll_event() + self.state = ConnectionFlowState.connected + + def _create_socket(self) -> None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(False) + self._socket_reader.resume() + + async def _poll_voice_ws(self, reconnect: bool) -> None: + backoff = ExponentialBackoff() + while True: + try: + await self.ws.poll_event() + except asyncio.CancelledError: + return + except (ConnectionClosed, asyncio.TimeoutError) as exc: + if isinstance(exc, ConnectionClosed): + # The following close codes are undocumented so I will document them here. + # 1000 - normal closure (obviously) + # 4014 - we were externally disconnected (voice channel deleted, we were moved, etc) + # 4015 - voice server has crashed + if exc.code in (1000, 4015): + # Don't call disconnect a second time if the websocket closed from a disconnect call + if not self._expecting_disconnect: + _log.info('Disconnecting from voice normally, close code %d.', exc.code) + await self.disconnect() + break + + if exc.code == 4014: + # We were disconnected by discord + # This condition is a race between the main ws event and the voice ws closing + if self._disconnected.is_set(): + _log.info('Disconnected from voice by discord, close code %d.', exc.code) + await self.disconnect() + break + + # We may have been moved to a different channel + _log.info('Disconnected from voice by force... potentially reconnecting.') + successful = await self._potential_reconnect() + if not successful: + _log.info('Reconnect was unsuccessful, disconnecting from voice normally...') + # Don't bother to disconnect if already disconnected + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + else: + continue + + _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') + + if not reconnect: + await self.disconnect() + raise + + retry = backoff.delay() + _log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) + await asyncio.sleep(retry) + await self.disconnect(cleanup=False) + + try: + await self._connect( + reconnect=reconnect, + timeout=self.timeout, + self_deaf=(self.self_voice_state or self).self_deaf, + self_mute=(self.self_voice_state or self).self_mute, + resume=False, + ) + except asyncio.TimeoutError: + # at this point we've retried 5 times... let's continue the loop. + _log.warning('Could not connect to voice... Retrying...') + continue + + async def _potential_reconnect(self) -> bool: + try: + await self._wait_for_state( + ConnectionFlowState.got_voice_server_update, + ConnectionFlowState.got_both_voice_updates, + ConnectionFlowState.disconnected, + timeout=self.timeout, + ) + except asyncio.TimeoutError: + return False + else: + if self.state is ConnectionFlowState.disconnected: + return False + + previous_ws = self.ws + try: + self.ws = await self._connect_websocket(False) + await self._handshake_websocket() + except (ConnectionClosed, asyncio.TimeoutError): + return False + else: + return True + finally: + await previous_ws.close() + + async def _move_to(self, channel: abc.Snowflake) -> None: + await self.voice_client.channel.guild.change_voice_state(channel=channel) + self.state = ConnectionFlowState.set_guild_voice_state + + def _update_voice_channel(self, channel_id: Optional[int]) -> None: + self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore From b9dc85e6f72f26dcc17f5d0808cfe35d439b3104 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 17 May 2024 19:31:26 +0200 Subject: [PATCH 054/354] Add type attribute to Invite --- discord/enums.py | 6 ++++++ discord/invite.py | 10 ++++++++-- discord/types/invite.py | 2 ++ docs/api.rst | 20 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index f7989a195e0a..80e27763d649 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -823,6 +823,12 @@ class PollLayoutType(Enum): default = 1 +class InviteType(Enum): + guild = 0 + group_dm = 1 + friend = 2 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/invite.py b/discord/invite.py index 1c18e41875d4..1d8dd1c8ef73 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -29,7 +29,7 @@ from .utils import parse_time, snowflake_time, _get_as_snowflake from .object import Object from .mixins import Hashable -from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, try_enum +from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, InviteType, try_enum from .appinfo import PartialAppInfo from .scheduled_event import ScheduledEvent @@ -296,6 +296,10 @@ class Invite(Hashable): Attributes ----------- + type: :class:`InviteType` + The type of the invite. + + .. versionadded: 2.4 max_age: Optional[:class:`int`] How long before the invite expires in seconds. A value of ``0`` indicates that it doesn't expire. @@ -374,6 +378,7 @@ class Invite(Hashable): 'expires_at', 'scheduled_event', 'scheduled_event_id', + 'type', ) BASE = 'https://discord.gg' @@ -387,6 +392,7 @@ def __init__( channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None, ): self._state: ConnectionState = state + self.type: InviteType = try_enum(InviteType, data.get('type', 0)) self.max_age: Optional[int] = data.get('max_age') self.code: str = data['code'] self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get('guild'), guild) @@ -496,7 +502,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return ( - f'' ) diff --git a/discord/types/invite.py b/discord/types/invite.py index b53ca374c6b2..f5f00078e950 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -35,6 +35,7 @@ from .appinfo import PartialAppInfo InviteTargetType = Literal[1, 2] +InviteType = Literal[0, 1, 2] class _InviteMetadata(TypedDict, total=False): @@ -63,6 +64,7 @@ class Invite(IncompleteInvite, total=False): target_type: InviteTargetType target_application: PartialAppInfo guild_scheduled_event: GuildScheduledEvent + type: InviteType class InviteWithCounts(Invite, _GuildPreviewUnique): diff --git a/docs/api.rst b/docs/api.rst index e8a00b7e4bda..87047f4638e3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3623,6 +3623,26 @@ of :class:`enum.Enum`. The default layout. + +.. class:: InviteType + + Represents the type of an invite. + + .. versionadded:: 2.4 + + .. attribute:: guild + + The invite is a guild invite. + + .. attribute:: group_dm + + The invite is a group DM invite. + + .. attribute:: friend + + The invite is a friend invite. + + .. _discord-api-audit-logs: Audit Log Data From b5ada0a6622438c702881dccbf6502f81ad7b4b9 Mon Sep 17 00:00:00 2001 From: Etwas <72528291+JeronimusII@users.noreply.github.com> Date: Sat, 18 May 2024 20:32:58 +0200 Subject: [PATCH 055/354] Fix merge methods for AppCommandContext and AppInstallationType --- discord/app_commands/installs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/app_commands/installs.py b/discord/app_commands/installs.py index 7d9b2f049245..5ac033245ab7 100644 --- a/discord/app_commands/installs.py +++ b/discord/app_commands/installs.py @@ -78,8 +78,8 @@ def user(self, value: bool) -> None: def merge(self, other: AppInstallationType) -> AppInstallationType: # Merging is similar to AllowedMentions where `self` is the base # and the `other` is the override preference - guild = self.guild if other.guild is None else other.guild - user = self.user if other.user is None else other.user + guild = self._guild if other._guild is None else other._guild + user = self._user if other._user is None else other._user return AppInstallationType(guild=guild, user=user) def _is_unset(self) -> bool: @@ -170,9 +170,9 @@ def private_channel(self, value: bool) -> None: self._private_channel = bool(value) def merge(self, other: AppCommandContext) -> AppCommandContext: - guild = self.guild if other.guild is None else other.guild - dm_channel = self.dm_channel if other.dm_channel is None else other.dm_channel - private_channel = self.private_channel if other.private_channel is None else other.private_channel + guild = self._guild if other._guild is None else other._guild + dm_channel = self._dm_channel if other._dm_channel is None else other._dm_channel + private_channel = self._private_channel if other._private_channel is None else other._private_channel return AppCommandContext(guild=guild, dm_channel=dm_channel, private_channel=private_channel) def _is_unset(self) -> bool: From f77ba711ba21bc2f70b252134880ebac3856acf8 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 18 May 2024 20:33:26 +0200 Subject: [PATCH 056/354] Add reaction type to raw events and users iterator --- discord/enums.py | 5 +++++ discord/http.py | 5 +++++ discord/raw_models.py | 8 +++++++- discord/reaction.py | 17 +++++++++++++++-- discord/types/gateway.py | 4 +++- discord/types/message.py | 3 +++ docs/api.rst | 15 +++++++++++++++ 7 files changed, 53 insertions(+), 4 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 80e27763d649..a8edb2a71f3c 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -829,6 +829,11 @@ class InviteType(Enum): friend = 2 +class ReactionType(Enum): + normal = 0 + burst = 1 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/http.py b/discord/http.py index aab710580c53..608595fe3b89 100644 --- a/discord/http.py +++ b/discord/http.py @@ -941,6 +941,7 @@ def get_reaction_users( emoji: str, limit: int, after: Optional[Snowflake] = None, + type: Optional[message.ReactionType] = None, ) -> Response[List[user.User]]: r = Route( 'GET', @@ -955,6 +956,10 @@ def get_reaction_users( } if after: params['after'] = after + + if type is not None: + params['type'] = type + return self.request(r, params=params) def clear_reactions(self, channel_id: Snowflake, message_id: Snowflake) -> Response[None]: diff --git a/discord/raw_models.py b/discord/raw_models.py index 571be38f1f5a..8d3ad328fb4c 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -27,7 +27,7 @@ import datetime from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union -from .enums import ChannelType, try_enum +from .enums import ChannelType, try_enum, ReactionType from .utils import _get_as_snowflake from .app_commands import AppCommandPermissions from .colour import Colour @@ -221,6 +221,10 @@ class RawReactionActionEvent(_RawReprMixin): and if ``event_type`` is ``REACTION_ADD``. .. versionadded:: 2.0 + type: :class:`ReactionType` + The type of the reaction. + + .. versionadded:: 2.4 """ __slots__ = ( @@ -234,6 +238,7 @@ class RawReactionActionEvent(_RawReprMixin): 'message_author_id', 'burst', 'burst_colours', + 'type', ) def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: ReactionActionType) -> None: @@ -246,6 +251,7 @@ def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: R self.message_author_id: Optional[int] = _get_as_snowflake(data, 'message_author_id') self.burst: bool = data.get('burst', False) self.burst_colours: List[Colour] = [Colour.from_str(c) for c in data.get('burst_colours', [])] + self.type: ReactionType = try_enum(ReactionType, data['type']) try: self.guild_id: Optional[int] = int(data['guild_id']) diff --git a/discord/reaction.py b/discord/reaction.py index cd0fbef10268..9fd933b0a57a 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -27,6 +27,7 @@ from .user import User from .object import Object +from .enums import ReactionType # fmt: off __all__ = ( @@ -185,7 +186,7 @@ async def clear(self) -> None: await self.message.clear_reaction(self.emoji) async def users( - self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None + self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None, type: Optional[ReactionType] = None ) -> AsyncIterator[Union[Member, User]]: """Returns an :term:`asynchronous iterator` representing the users that have reacted to the message. @@ -220,6 +221,11 @@ async def users( reacted to the message. after: Optional[:class:`abc.Snowflake`] For pagination, reactions are sorted by member. + type: Optional[:class:`ReactionType`] + The type of reaction to return users from. + If not provided, Discord only returns users of reactions with type ``normal``. + + .. versionadded:: 2.4 Raises -------- @@ -251,7 +257,14 @@ async def users( state = message._state after_id = after.id if after else None - data = await state.http.get_reaction_users(message.channel.id, message.id, emoji, retrieve, after=after_id) + data = await state.http.get_reaction_users( + message.channel.id, + message.id, + emoji, + retrieve, + after=after_id, + type=type.value if type is not None else None, + ) if data: limit -= len(data) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index b79bd9ca9fb3..ff43a5f25e70 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -37,7 +37,7 @@ from .emoji import Emoji, PartialEmoji from .member import MemberWithUser from .snowflake import Snowflake -from .message import Message +from .message import Message, ReactionType from .sticker import GuildSticker from .appinfo import GatewayAppInfo, PartialAppInfo from .guild import Guild, UnavailableGuild @@ -104,6 +104,7 @@ class MessageReactionAddEvent(TypedDict): message_author_id: NotRequired[Snowflake] burst: bool burst_colors: NotRequired[List[str]] + type: ReactionType class MessageReactionRemoveEvent(TypedDict): @@ -113,6 +114,7 @@ class MessageReactionRemoveEvent(TypedDict): emoji: PartialEmoji guild_id: NotRequired[Snowflake] burst: bool + type: ReactionType class MessageReactionRemoveAllEvent(TypedDict): diff --git a/discord/types/message.py b/discord/types/message.py index 16912d628715..bdb3f10ef9e6 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -57,6 +57,9 @@ class ReactionCountDetails(TypedDict): normal: int +ReactionType = Literal[0, 1] + + class Reaction(TypedDict): count: int me: bool diff --git a/docs/api.rst b/docs/api.rst index 87047f4638e3..44173d8310d9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3643,6 +3643,21 @@ of :class:`enum.Enum`. The invite is a friend invite. +.. class:: ReactionType + + Represents the type of a reaction. + + .. versionadded:: 2.4 + + .. attribute:: normal + + A normal reaction. + + .. attribute:: burst + + A burst reaction, also known as a "super reaction". + + .. _discord-api-audit-logs: Audit Log Data From efe81a67fb55f9f2a67bb84810be729ed8f09bc3 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 22 May 2024 09:04:37 -0400 Subject: [PATCH 057/354] Fix Message.poll not prioritising API data over cached data --- discord/message.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/discord/message.py b/discord/message.py index ea62b87f6784..1d1a3c96c19c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1839,12 +1839,11 @@ def __init__( # This updates the poll so it has the counts, if the message # was previously cached. - self.poll: Optional[Poll] = state._get_poll(self.id) - if self.poll is None: - try: - self.poll = Poll._from_data(data=data['poll'], message=self, state=state) - except KeyError: - pass + self.poll: Optional[Poll] = None + try: + self.poll = Poll._from_data(data=data['poll'], message=self, state=state) + except KeyError: + self.poll = state._get_poll(self.id) try: # if the channel doesn't have a guild attribute, we handle that From d18f14c173f84dc58f51476da9833f000a208fab Mon Sep 17 00:00:00 2001 From: z03h <7235242+z03h@users.noreply.github.com> Date: Wed, 29 May 2024 20:24:28 -0700 Subject: [PATCH 058/354] [commands] fix HelpCommand not carrying over checks update command impl over creating new one --- discord/ext/commands/help.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 163bc2694936..d06fbd8bf27d 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -297,6 +297,11 @@ def _eject_cog(self) -> None: # Revert `on_error` to use the original one in case of race conditions self.on_error = self._injected.on_help_command_error + def update(self, **kwargs: Any) -> None: + cog = self.cog + self.__init__(self._original, **dict(self.__original_kwargs__, **kwargs)) + self.cog = cog + class HelpCommand: r"""The base implementation for help command formatting. @@ -377,9 +382,8 @@ def copy(self) -> Self: return obj def _add_to_bot(self, bot: BotBase) -> None: - command = _HelpCommandImpl(self, **self.command_attrs) - bot.add_command(command) - self._command_impl = command + self._command_impl.update(**self.command_attrs) + bot.add_command(self._command_impl) def _remove_from_bot(self, bot: BotBase) -> None: bot.remove_command(self._command_impl.name) From 205ddaff66b94f7da8affa9c9e6a3ec3a5537332 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 30 May 2024 05:27:26 +0200 Subject: [PATCH 059/354] Update Polls event docs --- discord/poll.py | 7 ++++++- docs/api.rst | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/discord/poll.py b/discord/poll.py index b8f036aec268..c523f16098f7 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -446,7 +446,12 @@ def expires_at(self) -> Optional[datetime.datetime]: @property def created_at(self) -> Optional[datetime.datetime]: - """:class:`datetime.datetime`: Returns the poll's creation time, or ``None`` if user-created.""" + """Optional[:class:`datetime.datetime`]: Returns the poll's creation time. + + .. note:: + + This will **always** be ``None`` for stateless polls. + """ if not self._message: return diff --git a/docs/api.rst b/docs/api.rst index 44173d8310d9..41cf6549d169 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1053,8 +1053,8 @@ Polls .. function:: on_poll_vote_add(user, answer) on_poll_vote_remove(user, answer) - Called when a :class:`Poll` gains or loses a vote. If the ``user`` or ``message`` - are not cached then this event will not be called. + Called when a :class:`Poll` gains or loses a vote. If the ``user`` or ``answer``'s poll + parent message are not cached then this event will not be called. This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. @@ -1078,6 +1078,11 @@ Polls This requires :attr:`Intents.message_content` and :attr:`Intents.polls` to be enabled. + .. note:: + + If the poll allows multiple answers and the user removes or adds multiple votes, this + event will be called as many times as votes that are added or removed. + .. versionadded:: 2.4 :param payload: The raw event payload data. From 2c197649c20d6d188e495606d192c977939c692e Mon Sep 17 00:00:00 2001 From: Eric Schneider <16943959+tailoric@users.noreply.github.com> Date: Thu, 30 May 2024 13:24:56 +0200 Subject: [PATCH 060/354] Add note about archiver_id --- discord/threads.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/threads.py b/discord/threads.py index bbf476dc80ec..27288693457b 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -121,6 +121,10 @@ class Thread(Messageable, Hashable): This is always ``True`` for public threads. archiver_id: Optional[:class:`int`] The user's ID that archived this thread. + + .. note:: + Due to an API change, the ``archiver_id`` will always be ``None`` and can only be obtained via the audit log. + auto_archive_duration: :class:`int` The duration in minutes until the thread is automatically hidden from the channel list. Usually a value of 60, 1440, 4320 and 10080. From cc32fb364b8730d5a43df041881f549914039e8d Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 31 May 2024 04:51:51 +0200 Subject: [PATCH 061/354] Add notes about contexts and installation_types availability --- discord/app_commands/commands.py | 6 +++++ discord/app_commands/tree.py | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 50a573e8db54..e8c9c3c6feaa 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2372,6 +2372,12 @@ def guilds(*guild_ids: Union[Snowflake, int]) -> Callable[[T], T]: with the :meth:`CommandTree.command` or :meth:`CommandTree.context_menu` decorator then this must go below that decorator. + .. note :: + + Due to a Discord limitation, this decorator cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + Example: .. code-block:: python3 diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index abd8924806fd..bc0d68ec7938 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -307,10 +307,24 @@ def add_command( guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + override: :class:`bool` Whether to override a command with the same name. If ``False`` an exception is raised. Default is ``False``. @@ -877,10 +891,24 @@ def command( guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + auto_locale_strings: :class:`bool` If this is set to ``True``, then all translatable strings will implicitly be wrapped into :class:`locale_str` rather than :class:`str`. This could @@ -960,10 +988,24 @@ async def ban(interaction: discord.Interaction, user: discord.Member): guild: Optional[:class:`~discord.abc.Snowflake`] The guild to add the command to. If not given or ``None`` then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + guilds: List[:class:`~discord.abc.Snowflake`] The list of guilds to add the command to. This cannot be mixed with the ``guild`` parameter. If no guilds are given at all then it becomes a global command instead. + + .. note :: + + Due to a Discord limitation, this keyword argument cannot be used in conjunction with + contexts (e.g. :func:`.app_commands.allowed_contexts`) or installation types + (e.g. :func:`.app_commands.allowed_installs`). + auto_locale_strings: :class:`bool` If this is set to ``True``, then all translatable strings will implicitly be wrapped into :class:`locale_str` rather than :class:`str`. This could From 51142743bce60d60ed89fddfbb6e319c643ffdfd Mon Sep 17 00:00:00 2001 From: Leonardo Date: Fri, 31 May 2024 04:52:19 +0200 Subject: [PATCH 062/354] Fix bug with cache superfluously incrementing role position --- discord/guild.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 59c216f2f768..17f351b7a2f2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -432,27 +432,11 @@ def _update_voice_state(self, data: GuildVoiceState, channel_id: int) -> Tuple[O return member, before, after def _add_role(self, role: Role, /) -> None: - # roles get added to the bottom (position 1, pos 0 is @everyone) - # so since self.roles has the @everyone role, we can't increment - # its position because it's stuck at position 0. Luckily x += False - # is equivalent to adding 0. So we cast the position to a bool and - # increment it. - for r in self._roles.values(): - r.position += not r.is_default() - self._roles[role.id] = role def _remove_role(self, role_id: int, /) -> Role: # this raises KeyError if it fails.. - role = self._roles.pop(role_id) - - # since it didn't, we can change the positions now - # basically the same as above except we only decrement - # the position if we're above the role we deleted. - for r in self._roles.values(): - r.position -= r.position > role.position - - return role + return self._roles.pop(role_id) @classmethod def _create_unavailable(cls, *, state: ConnectionState, guild_id: int, data: Optional[Dict[str, Any]]) -> Guild: From 356474ffb95c4102ef3e0b337b5de22e96795a74 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Jun 2024 09:48:50 +0200 Subject: [PATCH 063/354] Add ButtonStyle.premium --- discord/components.py | 13 +++++++++++++ discord/enums.py | 3 ++- discord/interactions.py | 32 -------------------------------- discord/types/components.py | 3 ++- discord/ui/button.py | 30 ++++++++++++++++++++++++++++++ docs/interactions/api.rst | 5 +++++ 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/discord/components.py b/discord/components.py index 43a8f6ffc6bf..2af2d6d20d8b 100644 --- a/discord/components.py +++ b/discord/components.py @@ -170,6 +170,10 @@ class Button(Component): The label of the button, if any. emoji: Optional[:class:`PartialEmoji`] The emoji of the button, if available. + sku_id: Optional[:class:`int`] + The SKU ID this button sends you to, if available. + + .. versionadded:: 2.4 """ __slots__: Tuple[str, ...] = ( @@ -179,6 +183,7 @@ class Button(Component): 'disabled', 'label', 'emoji', + 'sku_id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -195,6 +200,11 @@ def __init__(self, data: ButtonComponentPayload, /) -> None: except KeyError: self.emoji = None + try: + self.sku_id: Optional[int] = int(data['sku_id']) + except KeyError: + self.sku_id = None + @property def type(self) -> Literal[ComponentType.button]: """:class:`ComponentType`: The type of component.""" @@ -207,6 +217,9 @@ def to_dict(self) -> ButtonComponentPayload: 'disabled': self.disabled, } + if self.sku_id: + payload['sku_id'] = str(self.sku_id) + if self.label: payload['label'] = self.label diff --git a/discord/enums.py b/discord/enums.py index a8edb2a71f3c..eaf8aef5e058 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -603,7 +603,7 @@ class InteractionResponseType(Enum): message_update = 7 # for components autocomplete_result = 8 modal = 9 # for modals - premium_required = 10 + # premium_required = 10 (deprecated) class VideoQualityMode(Enum): @@ -635,6 +635,7 @@ class ButtonStyle(Enum): success = 3 danger = 4 link = 5 + premium = 6 # Aliases blurple = 1 diff --git a/discord/interactions.py b/discord/interactions.py index 5d9d2a6808b9..e0fb7ed8654b 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -1050,38 +1050,6 @@ async def send_modal(self, modal: Modal, /) -> None: self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal - async def require_premium(self) -> None: - """|coro| - - Sends a message to the user prompting them that a premium purchase is required for this interaction. - - This type of response is only available for applications that have a premium SKU set up. - - Raises - ------- - HTTPException - Sending the response failed. - InteractionResponded - This interaction has already been responded to before. - """ - if self._response_type: - raise InteractionResponded(self._parent) - - parent = self._parent - adapter = async_context.get() - http = parent._state.http - - params = interaction_response_params(InteractionResponseType.premium_required.value) - await adapter.create_interaction_response( - parent.id, - parent.token, - session=parent._session, - proxy=http.proxy, - proxy_auth=http.proxy_auth, - params=params, - ) - self._response_type = InteractionResponseType.premium_required - async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None: """|coro| diff --git a/discord/types/components.py b/discord/types/components.py index 218f5cef07bf..3b1295c1393c 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -31,7 +31,7 @@ from .channel import ChannelType ComponentType = Literal[1, 2, 3, 4] -ButtonStyle = Literal[1, 2, 3, 4, 5] +ButtonStyle = Literal[1, 2, 3, 4, 5, 6] TextStyle = Literal[1, 2] DefaultValueType = Literal['user', 'role', 'channel'] @@ -49,6 +49,7 @@ class ButtonComponent(TypedDict): disabled: NotRequired[bool] emoji: NotRequired[PartialEmoji] label: NotRequired[str] + sku_id: NotRequired[str] class SelectOption(TypedDict): diff --git a/discord/ui/button.py b/discord/ui/button.py index 28238a6f06aa..4d306fd10c1a 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -77,6 +77,10 @@ class Button(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + sku_id: Optional[:class:`int`] + The SKU ID this button sends you to. Can't be combined with ``url``. + + .. versionadded:: 2.4 """ __item_repr_attributes__: Tuple[str, ...] = ( @@ -86,6 +90,7 @@ class Button(Item[V]): 'label', 'emoji', 'row', + 'sku_id', ) def __init__( @@ -98,6 +103,7 @@ def __init__( url: Optional[str] = None, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, + sku_id: Optional[int] = None, ): super().__init__() if custom_id is not None and url is not None: @@ -113,6 +119,9 @@ def __init__( if url is not None: style = ButtonStyle.link + if sku_id is not None: + style = ButtonStyle.premium + if emoji is not None: if isinstance(emoji, str): emoji = PartialEmoji.from_str(emoji) @@ -128,6 +137,7 @@ def __init__( label=label, style=style, emoji=emoji, + sku_id=sku_id, ) self.row = row @@ -202,6 +212,19 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: else: self._underlying.emoji = None + @property + def sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: The SKU ID this button sends you to. + + .. versionadded:: 2.4 + """ + return self._underlying.sku_id + + @sku_id.setter + def sku_id(self, value: Optional[int]) -> None: + self.style = ButtonStyle.premium + self._underlying.sku_id = value + @classmethod def from_component(cls, button: ButtonComponent) -> Self: return cls( @@ -212,6 +235,7 @@ def from_component(cls, button: ButtonComponent) -> Self: url=button.url, emoji=button.emoji, row=None, + sku_id=button.sku_id, ) @property @@ -241,6 +265,7 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, + sku_id: Optional[int] = None, ) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: """A decorator that attaches a button to a component. @@ -278,6 +303,10 @@ def button( like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + sku_id: Optional[:class:`int`] + The SKU ID this button sends you to. Can't be combined with ``url``. + + .. versionadded:: 2.4 """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: @@ -293,6 +322,7 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto 'label': label, 'emoji': emoji, 'row': row, + 'sku_id': sku_id, } return func diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 53ad210b3e6c..d467d2b9f101 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -334,7 +334,12 @@ Enumerations .. attribute:: link Represents a link button. + .. attribute:: premium + Represents a gradient button denoting that buying a SKU is + required to perform this action. + + .. versionadded:: 2.4 .. attribute:: blurple An alias for :attr:`primary`. From 0e58a927ddbc300a17ef0137d948faa659565313 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 2 Jun 2024 15:00:24 -0400 Subject: [PATCH 064/354] [commands] Add support for channel URLs in channel converter Fix #9799 --- discord/ext/commands/converter.py | 63 ++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 41e0f6c4a5b5..830c58662bc4 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -438,19 +438,36 @@ class GuildChannelConverter(IDConverter[discord.abc.GuildChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name. + 3. Lookup by channel URL. + 4. Lookup by name. .. versionadded:: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.abc.GuildChannel: return self._resolve_channel(ctx, argument, 'channels', discord.abc.GuildChannel) + @staticmethod + def _parse_from_url(argument: str) -> Optional[re.Match[str]]: + link_regex = re.compile( + r'https?://(?:(?:ptb|canary|www)\.)?discord(?:app)?\.com/channels/' + r'(?:[0-9]{15,20}|@me)' + r'/([0-9]{15,20})(?:/(?:[0-9]{15,20})/?)?$' + ) + return link_regex.match(argument) + @staticmethod def _resolve_channel(ctx: Context[BotT], argument: str, attribute: str, type: Type[CT]) -> CT: bot = ctx.bot - match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument) + match = ( + IDConverter._get_id_match(argument) + or re.match(r'<#([0-9]{15,20})>$', argument) + or GuildChannelConverter._parse_from_url(argument) + ) result = None guild = ctx.guild @@ -480,7 +497,11 @@ def check(c): @staticmethod def _resolve_thread(ctx: Context[BotT], argument: str, attribute: str, type: Type[TT]) -> TT: - match = IDConverter._get_id_match(argument) or re.match(r'<#([0-9]{15,20})>$', argument) + match = ( + IDConverter._get_id_match(argument) + or re.match(r'<#([0-9]{15,20})>$', argument) + or GuildChannelConverter._parse_from_url(argument) + ) result = None guild = ctx.guild @@ -510,10 +531,14 @@ class TextChannelConverter(IDConverter[discord.TextChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.TextChannel: @@ -530,10 +555,14 @@ class VoiceChannelConverter(IDConverter[discord.VoiceChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.VoiceChannel: @@ -552,7 +581,11 @@ class StageChannelConverter(IDConverter[discord.StageChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.StageChannel: @@ -569,7 +602,11 @@ class CategoryChannelConverter(IDConverter[discord.CategoryChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. .. versionchanged:: 1.5 Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` @@ -588,9 +625,13 @@ class ThreadConverter(IDConverter[discord.Thread]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name. + 3. Lookup by channel URL. + 4. Lookup by name. .. versionadded: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.Thread: @@ -607,9 +648,13 @@ class ForumChannelConverter(IDConverter[discord.ForumChannel]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name + 3. Lookup by channel URL. + 4. Lookup by name .. versionadded:: 2.0 + + .. versionchanged:: 2.4 + Add lookup by channel URL, accessed via "Copy Link" in the Discord client within channels. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.ForumChannel: From fdb17ead039f1efefbdaebcc47c6c4cd22ecae40 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 11 Jun 2024 23:30:53 +0200 Subject: [PATCH 065/354] Fix exempt_channels not being passed along in create_automod_rule --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 17f351b7a2f2..8dca0f4986d4 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4374,7 +4374,7 @@ async def create_automod_rule( actions=[a.to_dict() for a in actions], enabled=enabled, exempt_roles=[str(r.id) for r in exempt_roles] if exempt_roles else None, - exempt_channel=[str(c.id) for c in exempt_channels] if exempt_channels else None, + exempt_channels=[str(c.id) for c in exempt_channels] if exempt_channels else None, reason=reason, ) From 895d6c37df33a541a4be366244cdcac5cab3568e Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 11 Jun 2024 23:31:08 +0200 Subject: [PATCH 066/354] Add Permissions.use_external_apps --- discord/permissions.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/discord/permissions.py b/discord/permissions.py index 39b0b1a5e4c8..9fb46bcba7ef 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -241,10 +241,10 @@ def all_channel(cls) -> Self: Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. .. versionchanged:: 2.4 - Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, and - :attr:`use_embedded_activities` permissions. + Added :attr:`send_polls`, :attr:`send_voice_messages`, attr:`use_external_sounds`, + :attr:`use_embedded_activities`, and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0010_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) + return cls(0b0000_0000_0000_0110_0110_0100_1111_1101_1011_0011_1111_0111_1111_1111_0101_0001) @classmethod def general(cls) -> Self: @@ -291,9 +291,9 @@ def text(cls) -> Self: Added :attr:`send_voice_messages` permission. .. versionchanged:: 2.4 - Added :attr:`send_polls` permission. + Added :attr:`send_polls` and :attr:`use_external_apps` permissions. """ - return cls(0b0000_0000_0000_0010_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) + return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) @classmethod def voice(cls) -> Self: @@ -760,6 +760,14 @@ def create_polls(self) -> int: """ return 1 << 49 + @flag_value + def use_external_apps(self) -> int: + """:class:`bool`: Returns ``True`` if a user can use external apps. + + .. versionadded:: 2.4 + """ + return 1 << 50 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -882,6 +890,7 @@ class PermissionOverwrite: create_events: Optional[bool] send_polls: Optional[bool] create_polls: Optional[bool] + use_external_apps: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} From bb1d09a13f23641cd48f06352e58983bbff8cbed Mon Sep 17 00:00:00 2001 From: Kile <69253692+Kile@users.noreply.github.com> Date: Wed, 12 Jun 2024 20:25:49 +0200 Subject: [PATCH 067/354] Account for user installations in Channel.permissions_for Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> --- discord/abc.py | 14 ++++++++++++++ discord/permissions.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 656a38659f90..d9dc1a14ca8a 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -699,6 +699,7 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: - Member overrides - Implicit permissions - Member timeout + - User installed app If a :class:`~discord.Role` is passed, then it checks the permissions someone with that role would have, which is essentially: @@ -714,6 +715,12 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: .. versionchanged:: 2.0 ``obj`` parameter is now positional-only. + .. versionchanged:: 2.4 + User installed apps are now taken into account. + The permissions returned for a user installed app mirrors the + permissions Discord returns in :attr:`~discord.Interaction.app_permissions`, + though it is recommended to use that attribute instead. + Parameters ---------- obj: Union[:class:`~discord.Member`, :class:`~discord.Role`] @@ -745,6 +752,13 @@ def permissions_for(self, obj: Union[Member, Role], /) -> Permissions: return Permissions.all() default = self.guild.default_role + if default is None: + + if self._state.self_id == obj.id: + return Permissions._user_installed_permissions(in_guild=True) + else: + return Permissions.none() + base = Permissions(default.permissions.value) # Handle the role case first diff --git a/discord/permissions.py b/discord/permissions.py index 9fb46bcba7ef..17c7b38c95dc 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -208,6 +208,22 @@ def _dm_permissions(cls) -> Self: base.send_messages_in_threads = False return base + @classmethod + def _user_installed_permissions(cls, *, in_guild: bool) -> Self: + base = cls.none() + base.send_messages = True + base.attach_files = True + base.embed_links = True + base.external_emojis = True + base.send_voice_messages = True + if in_guild: + # Logically this is False but if not set to True, + # permissions just become 0. + base.read_messages = True + base.send_tts_messages = True + base.send_messages_in_threads = True + return base + @classmethod def all_channel(cls) -> Self: """A :class:`Permissions` with all channel-specific permissions set to From 837bc35b87efa2bbbe8ce29b2b16216b7357a4b1 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 12 Jun 2024 14:42:12 -0400 Subject: [PATCH 068/354] Add missing versionadded for a few decorators --- discord/app_commands/commands.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index e8c9c3c6feaa..cd6eafaf3de9 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2549,6 +2549,8 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]] Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2640,6 +2642,8 @@ def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channe Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2691,6 +2695,8 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2739,6 +2745,8 @@ def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- @@ -2781,6 +2789,8 @@ def allowed_installs( Due to a Discord limitation, this decorator does nothing in subcommands and is ignored. + .. versionadded:: 2.4 + Examples --------- From fb12d3d546da858568c627073f0a3026a73b2536 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 13 Jun 2024 23:31:14 -0400 Subject: [PATCH 069/354] Remove unnecessary warning logs for poll events --- discord/state.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/discord/state.py b/discord/state.py index 032dc2645c7c..db0395266b6e 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1648,13 +1648,8 @@ def parse_message_poll_vote_add(self, data: gw.PollVoteActionEvent) -> None: if message and user: poll = self._update_poll_counts(message, raw.answer_id, True, raw.user_id == self.self_id) - if not poll: - _log.warning( - 'POLL_VOTE_ADD referencing message with ID: %s does not have a poll. Discarding.', raw.message_id - ) - return - - self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) + if poll: + self.dispatch('poll_vote_add', user, poll.get_answer(raw.answer_id)) def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: raw = RawPollVoteActionEvent(data) @@ -1671,13 +1666,8 @@ def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: if message and user: poll = self._update_poll_counts(message, raw.answer_id, False, raw.user_id == self.self_id) - if not poll: - _log.warning( - 'POLL_VOTE_REMOVE referencing message with ID: %s does not have a poll. Discarding.', raw.message_id - ) - return - - self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) + if poll: + self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): From 9eac36501ae291be08a85af9161869376dde0020 Mon Sep 17 00:00:00 2001 From: Michael H Date: Fri, 14 Jun 2024 18:08:12 -0400 Subject: [PATCH 070/354] Allow deletion race conditions to work with purge --- discord/abc.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index d9dc1a14ca8a..fec57b52af98 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -49,7 +49,7 @@ from .object import OLDEST_OBJECT, Object from .context_managers import Typing from .enums import ChannelType, InviteTarget -from .errors import ClientException +from .errors import ClientException, NotFound from .mentions import AllowedMentions from .permissions import PermissionOverwrite, Permissions from .role import Role @@ -122,7 +122,14 @@ def __repr__(self) -> str: async def _single_delete_strategy(messages: Iterable[Message], *, reason: Optional[str] = None): for m in messages: - await m.delete() + try: + await m.delete() + except NotFound as exc: + if exc.code == 10008: + continue # bulk deletion ignores not found messages, single deletion does not. + # several other race conditions with deletion should fail without continuing, + # such as the channel being deleted and not found. + raise async def _purge_helper( From be9edf8deb6b41e201070670a9e177eb3392d50e Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 15 Jun 2024 13:38:58 +0200 Subject: [PATCH 071/354] Remove setting sku_id explicitly via button decorator Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/ui/button.py | 19 ++++++++----------- docs/interactions/api.rst | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index 4d306fd10c1a..f986b078b988 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -78,7 +78,8 @@ class Button(Item[V]): For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). sku_id: Optional[:class:`int`] - The SKU ID this button sends you to. Can't be combined with ``url``. + The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` + nor ``custom_id``. .. versionadded:: 2.4 """ @@ -222,7 +223,8 @@ def sku_id(self) -> Optional[int]: @sku_id.setter def sku_id(self, value: Optional[int]) -> None: - self.style = ButtonStyle.premium + if value is not None: + self.style = ButtonStyle.premium self._underlying.sku_id = value @classmethod @@ -265,7 +267,6 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, - sku_id: Optional[int] = None, ) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: """A decorator that attaches a button to a component. @@ -275,11 +276,11 @@ def button( .. note:: - Buttons with a URL cannot be created with this function. + Buttons with a URL or an SKU cannot be created with this function. Consider creating a :class:`Button` manually instead. - This is because buttons with a URL do not have a callback + This is because these buttons cannot have a callback associated with them since Discord does not do any processing - with it. + with them. Parameters ------------ @@ -303,10 +304,6 @@ def button( like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). - sku_id: Optional[:class:`int`] - The SKU ID this button sends you to. Can't be combined with ``url``. - - .. versionadded:: 2.4 """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: @@ -322,7 +319,7 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto 'label': label, 'emoji': emoji, 'row': row, - 'sku_id': sku_id, + 'sku_id': None, } return func diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index d467d2b9f101..211cd790f6b5 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -336,7 +336,7 @@ Enumerations Represents a link button. .. attribute:: premium - Represents a gradient button denoting that buying a SKU is + Represents a button denoting that buying a SKU is required to perform this action. .. versionadded:: 2.4 From c055fd32bbe5c68b144a7ac938b911035eb6d3e1 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 15 Jun 2024 07:46:36 -0400 Subject: [PATCH 072/354] Fix ui.Button providing a custom_id for premium buttons --- discord/ui/button.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index f986b078b988..43bd3a8b0f9d 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -107,11 +107,15 @@ def __init__( sku_id: Optional[int] = None, ): super().__init__() - if custom_id is not None and url is not None: - raise TypeError('cannot mix both url and custom_id with Button') + if custom_id is not None and (url is not None or sku_id is not None): + raise TypeError('cannot mix both url or sku_id and custom_id with Button') + if url is not None and sku_id is not None: + raise TypeError('cannot mix both url and sku_id') + + requires_custom_id = url is None and sku_id is None self._provided_custom_id = custom_id is not None - if url is None and custom_id is None: + if requires_custom_id and custom_id is None: custom_id = os.urandom(16).hex() if custom_id is not None and not isinstance(custom_id, str): From d528e8f8b5bd686ed1fe7159dde0ea936091c21f Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 21 Jun 2024 08:14:03 -0400 Subject: [PATCH 073/354] Add information on Message.poll for Message Content Intent --- docs/intents.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/intents.rst b/docs/intents.rst index e805c5ff71ec..ca85ab8ddc04 100644 --- a/docs/intents.rst +++ b/docs/intents.rst @@ -114,6 +114,7 @@ Message Content - Whether you use :attr:`Message.attachments` to check message attachments. - Whether you use :attr:`Message.embeds` to check message embeds. - Whether you use :attr:`Message.components` to check message components. +- Whether you use :attr:`Message.poll` to check the message polls. - Whether you use the commands extension with a non-mentioning prefix. .. _intents_member_cache: From c75ca25e03796194610db656d43d5aa8792fcf73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Sat, 22 Jun 2024 01:47:32 +0100 Subject: [PATCH 074/354] Add changelog for v2.4.0 Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- docs/whats_new.rst | 186 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 4f09e0a04c8b..d51de610b90a 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,192 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p4p0: + +v2.4.0 +------- + +New Features +~~~~~~~~~~~~~ + +- Add support for allowed contexts in app commands (:issue:`9760`). + - An "allowed context" is the location where an app command can be used. + - This is an internal change to decorators such as :func:`app_commands.guild_only` and :func:`app_commands.dm_only`. + - Add :func:`app_commands.private_channel_only`. + - Add :func:`app_commands.allowed_contexts`. + - Add :class:`app_commands.AppCommandContext`. + - Add :attr:`app_commands.Command.allowed_contexts`. + - Add :attr:`app_commands.AppCommand.allowed_contexts`. + - Add :attr:`app_commands.ContextMenu.allowed_contexts`. + +- Add support for user-installable apps (:issue:`9760`). + - Add :attr:`app_commands.Command.allowed_installs`. + - Add :attr:`app_commands.AppCommand.allowed_installs`. + - Add :attr:`app_commands.ContextMenu.allowed_installs`. + - Add :func:`app_commands.allowed_installs`. + - Add :func:`app_commands.guild_install`. + - Add :func:`app_commands.user_install`. + - Add :class:`app_commands.AppInstallationType`. + - Add :attr:`Interaction.context`. + - Add :meth:`Interaction.is_guild_integration`. + - Add :meth:`Interaction.is_user_integration`. + +- Add support for Polls (:issue:`9759`). + - Polls can be created using :class:`Poll` and the ``poll`` keyword-only parameter in various message sending methods. + - Add :class:`PollAnswer` and :class:`PollMedia`. + - Add :attr:`Intents.polls`, :attr:`Intents.guild_polls` and :attr:`Intents.dm_polls` intents. + - Add :meth:`Message.end_poll` method to end polls. + - Add new events, :func:`on_poll_vote_add`, :func:`on_poll_vote_remove`, :func:`on_raw_poll_vote_add`, and :func:`on_raw_poll_vote_remove`. + +- Voice handling has been completely rewritten to hopefully fix many bugs (:issue:`9525`, :issue:`9528`, :issue:`9536`, :issue:`9572`, :issue:`9576`, :issue:`9596`, :issue:`9683`, :issue:`9699`, :issue:`9772`, etc.) +- Add :attr:`DMChannel.recipients` to get all recipients of a DM channel (:issue:`9760`). +- Add support for :attr:`RawReactionActionEvent.message_author_id`. +- Add support for :attr:`AuditLogAction.creator_monetization_request_created` and :attr:`AuditLogAction.creator_monetization_terms_accepted`. +- Add support for :class:`AttachmentFlags`, accessed via :attr:`Attachment.flags` (:issue:`9486`). +- Add support for :class:`RoleFlags`, accessed via :attr:`Role.flags` (:issue:`9485`). +- Add support for :attr:`ChannelType.media`, accessed via :meth:`ForumChannel.is_media`. +- Add various new permissions (:issue:`9501`, :issue:`9762`, :issue:`9759`, :issue:`9857`) + - Add :meth:`Permissions.events`. + - Add :attr:`Permissions.create_events`. + - Add :attr:`Permissions.view_creator_monetization_analytics`. + - Add :attr:`Permissions.send_polls` + - Add :attr:`Permissions.create_polls`. + - Add :attr:`Permissions.use_external_apps`. + +- Add shortcut for :attr:`CategoryChannel.forums`. +- Add encoder options to :meth:`VoiceClient.play` (:issue:`9527`). +- Add support for team member roles. + - Add :class:`TeamMemberRole`. + - Add :attr:`TeamMember.role`. + - Updated :attr:`Bot.owner_ids <.ext.commands.Bot.owner_ids>` to account for team roles. Team owners or developers are considered Bot owners. + +- Add optional attribute ``integration_type`` in :attr:`AuditLogEntry.extra` for ``kick`` or ``member_role_update`` actions. +- Add support for "dynamic" :class:`ui.Item` that let you parse state out of a ``custom_id`` using regex. + - In order to use this, you must subclass :class:`ui.DynamicItem`. + - This is an alternative to persistent views. + - Add :meth:`Client.add_dynamic_items`. + - Add :meth:`Client.remove_dynamic_items`. + - Add :meth:`ui.Item.interaction_check`. + - Check the :resource:`dynamic_counter example ` for more information. + +- Add support for reading burst reactions. The API does not support sending them as of currently. + - Add :attr:`Reaction.normal_count`. + - Add :attr:`Reaction.burst_count`. + - Add :attr:`Reaction.me_burst`. + +- Add support for default values on select menus (:issue:`9577`). + - Add :class:`SelectDefaultValue`. + - Add :class:`SelectDefaultValueType`. + - Add a ``default_values`` attribute to each specialised select menu. + +- Add ``scheduled_event`` parameter for :meth:`StageChannel.create_instance` (:issue:`9595`). +- Add support for auto mod members (:issue:`9328`). + - Add ``type`` keyword argument to :class:`AutoModRuleAction`. + - Add :attr:`AutoModTrigger.mention_raid_protection`. + - Add :attr:`AutoModRuleTriggerType.member_profile`. + - Add :attr:`AutoModRuleEventType.member_update`. + - Add :attr:`AutoModRuleActionType.block_member_interactions`. + +- Add support for premium app integrations (:issue:`9453`). + - Add multiple SKU and entitlement related classes, e.g. :class:`SKU`, :class:`Entitlement`, :class:`SKUFlags`. + - Add multiple enums, e.g. :class:`SKUType`, :class:`EntitlementType`, :class:`EntitlementOwnerType`. + - Add :meth:`Client.fetch_skus` and :meth:`Client.fetch_entitlement` to fetch from the API. + - Add :meth:`Client.create_entitlement` to create entitlements. + - Add :attr:`Client.entitlements`. + - Add :attr:`Interaction.entitlement_sku_ids`. + - Add :attr:`Interaction.entitlements`. + - Add :attr:`ButtonStyle.premium` and :attr:`ui.Button.sku_id` to send a button asking the user to buy an SKU (:issue:`9845`). + - Add support for one time purchase (:issue:`9803`). + +- Add support for editing application info (:issue:`9610`). + - Add :attr:`AppInfo.interactions_endpoint_url`. + - Add :attr:`AppInfo.redirect_uris`. + - Add :meth:`AppInfo.edit`. + +- Add support for getting/fetching threads from :class:`Message` (:issue:`9665`). + - Add :attr:`PartialMessage.thread`. + - Add :attr:`Message.thread`. + - Add :meth:`Message.fetch_thread`. + +- Add support for platform and assets to activities (:issue:`9677`). + - Add :attr:`Activity.platform`. + - Add :attr:`Game.platform`. + - Add :attr:`Game.assets`. + +- Add support for suppressing embeds in an interaction response (:issue:`9678`). +- Add support for adding forum thread tags via webhook (:issue:`9680`) and (:issue:`9783`). +- Add support for guild incident message types (:issue:`9686`). +- Add :attr:`Locale.latin_american_spanish` (:issue:`9689`). +- Add support for setting voice channel status (:issue:`9603`). +- Add a shard connect timeout parameter to :class:`AutoShardedClient`. +- Add support for guild incidents (:issue:`9590`). + - Updated :meth:`Guild.edit` with ``invites_disabled_until`` and ``dms_disabled_until`` parameters. + - Add :attr:`Guild.invites_paused_until`. + - Add :attr:`Guild.dms_paused_until`. + - Add :meth:`Guild.invites_paused`. + - Add :meth:`Guild.dms_paused`. + +- Add support for :attr:`abc.User.avatar_decoration` (:issue:`9343`). +- Add support for GIF stickers (:issue:`9737`). +- Add support for updating :class:`ClientUser` banners (:issue:`9752`). +- Add support for bulk banning members via :meth:`Guild.bulk_ban`. +- Add ``reason`` keyword argument to :meth:`Thread.delete` (:issue:`9804`). +- Add :attr:`AppInfo.approximate_guild_count` (:issue:`9811`). +- Add support for :attr:`Message.interaction_metadata` (:issue:`9817`). +- Add support for differing :class:`Invite` types (:issue:`9682`). +- Add support for reaction types to raw and non-raw models (:issue:`9836`). +- |tasks| Add ``name`` parameter to :meth:`~ext.tasks.loop` to name the internal :class:`asyncio.Task`. +- |commands| Add fallback behaviour to :class:`~ext.commands.CurrentGuild`. +- |commands| Add logging for errors that occur during :meth:`~ext.commands.Cog.cog_unload`. +- |commands| Add support for :class:`typing.NewType` and ``type`` keyword type aliases (:issue:`9815`). + - Also supports application commands. + +- |commands| Add support for positional-only flag parameters (:issue:`9805`). +- |commands| Add support for channel URLs in ChannelConverter related classes (:issue:`9799`). + + +Bug Fixes +~~~~~~~~~~ + +- Fix emoji and sticker cache being populated despite turning the intent off. +- Fix outstanding chunk requests when receiving a gateway READY event not being cleared (:issue:`9571`). +- Fix escape behaviour for lists and headers in :meth:`~utils.escape_markdown`. +- Fix alias value for :attr:`Intents.auto_moderation` (:issue:`9524`). +- Fixes and improvements for :class:`FFmpegAudio` and all related subclasses (:issue:`9528`). +- Fix :meth:`Template.source_guild` attempting to resolve from cache (:issue:`9535`). +- Fix :exc:`IndexError` being raised instead of :exc:`ValueError` when calling :meth:`Colour.from_str` with an empty string (:issue:`9540`). +- Fix :meth:`View.from_message` not correctly creating the varying :class:`ui.Select` types (:issue:`9559`). +- Fix logging with autocomplete exceptions, which were previously suppressed. +- Fix possible error in voice cleanup logic (:issue:`9572`). +- Fix possible :exc:`AttributeError` during :meth:`app_commands.CommandTree.sync` when a command is regarded as 'too large'. +- Fix possible :exc:`TypeError` if a :class:`app_commands.Group` did not have a name set (:issue:`9581`). +- Fix possible bad voice state where you move to a voice channel with missing permissions (:issue:`9596`). +- Fix websocket reaching an error state due to received error payload (:issue:`9561`). +- Fix handling of :class:`AuditLogDiff` when relating to auto mod triggers (:issue:`9622`). +- Fix race condition in voice logic relating to disconnect and connect (:issue:`9683`). +- Use the :attr:`Interaction.user` guild as a fallback for :attr:`Interaction.guild` if not available. +- Fix restriction on auto moderation audit log ID range. +- Fix check for maximum number of children per :class:`ui.View`. +- Fix comparison between :class:`Object` classes with a ``type`` set. +- Fix handling of an enum in :meth:`AutoModRule.edit` (:issue:`9798`). +- Fix handling of :meth:`Client.close` within :meth:`Client.__aexit__` (:issue:`9769`). +- Fix channel deletion not evicting related threads from cache (:issue:`9796`). +- Fix bug with cache superfluously incrementing role positions (:issue:`9853`). +- Fix ``exempt_channels`` not being passed along in :meth:`Guild.create_automod_rule` (:issue:`9861`). +- Fix :meth:`abc.GuildChannel.purge` failing on single-message delete mode if the message was deleted (:issue:`9830`, :issue:`9863`). +- |commands| Fix localization support for :class:`~ext.commands.HybridGroup` fallback. +- |commands| Fix nested :class:`~ext.commands.HybridGroup`'s inserting manual app commands. +- |commands| Fix an issue where :class:`~ext.commands.HybridGroup` wrapped instances would be out of sync. +- |commands| Fix :class:`~ext.commands.HelpCommand` defined checks not carrying over during copy (:issue:`9843`). + +Miscellaneous +~~~~~~~~~~~~~~ + +- Additional documentation added for logging capabilities. +- Performance increases of constructing :class:`Permissions` using keyword arguments. +- Improve ``__repr__`` of :class:`SyncWebhook` and :class:`Webhook` (:issue:`9764`). +- Change internal thread names to be consistent (:issue:`9538`). + .. _vp2p3p2: v2.3.2 From 978a96b25c47a818289ddb9ecc31d78d6a80b2bf Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 21 Jun 2024 21:08:59 -0400 Subject: [PATCH 075/354] Version bump to v2.4.0 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index e3148e51378b..ea1518629f39 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.4.0a' +__version__ = '2.4.0' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -80,7 +80,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 3a8063602919813e91d19101f9b5a0e9e7438d4b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 21 Jun 2024 21:17:36 -0400 Subject: [PATCH 076/354] Version bump for development --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index ea1518629f39..765719b68731 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.4.0' +__version__ = '2.5.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -80,7 +80,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 6643784d3354d42408d08c11ed52215d7ac60ef2 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 5 Jul 2024 15:11:35 -0400 Subject: [PATCH 077/354] [commands] Clarify Converter.convert exception raising --- discord/ext/commands/converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 830c58662bc4..6c559009dbae 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -126,6 +126,10 @@ async def convert(self, ctx: Context[BotT], argument: str) -> T_co: raise a :exc:`.CommandError` derived exception as it will properly propagate to the error handlers. + Note that if this method is called manually, :exc:`Exception` + should be caught to handle the cases where a subclass does + not explicitly inherit from :exc:`.CommandError`. + Parameters ----------- ctx: :class:`.Context` From 97ca618570780a620888b271caaae7b682976323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jun-Ah=20=EC=A4=80=EC=95=84?= Date: Sat, 6 Jul 2024 05:18:01 +0900 Subject: [PATCH 078/354] Fix incorrect Select max option condition --- discord/ui/select.py | 2 +- tests/test_ui_selects.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/test_ui_selects.py diff --git a/discord/ui/select.py b/discord/ui/select.py index b7a8e694cf1c..6738b9727c56 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -499,7 +499,7 @@ def append_option(self, option: SelectOption) -> None: The number of options exceeds 25. """ - if len(self._underlying.options) > 25: + if len(self._underlying.options) >= 25: raise ValueError('maximum number of options already provided') self._underlying.options.append(option) diff --git a/tests/test_ui_selects.py b/tests/test_ui_selects.py new file mode 100644 index 000000000000..a9019c3de995 --- /dev/null +++ b/tests/test_ui_selects.py @@ -0,0 +1,39 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +@pytest.mark.asyncio +async def test_add_option(): + select = discord.ui.Select() + + for i in range(1, 25 + 1): + select.add_option(label=str(i), value=str(i)) + + with pytest.raises(ValueError): + select.add_option(label="26", value="26") From 7d3eff9d9d115dc29b5716c42eaeedf1a008e9b0 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 10 Jul 2024 07:55:16 -0400 Subject: [PATCH 079/354] Allow discord.Object use for permissions in channel creation --- discord/guild.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 8dca0f4986d4..f34818b63503 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1126,7 +1126,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, TextChannelPayload]: @@ -1137,7 +1137,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, VoiceChannelPayload]: @@ -1148,7 +1148,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.stage_voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, StageChannelPayload]: @@ -1159,7 +1159,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.category], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, CategoryChannelPayload]: @@ -1170,7 +1170,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.news], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, NewsChannelPayload]: @@ -1181,7 +1181,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.news, ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: @@ -1192,7 +1192,7 @@ def _create_channel( self, name: str, channel_type: Literal[ChannelType.forum], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, ForumChannelPayload]: @@ -1203,7 +1203,7 @@ def _create_channel( self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, GuildChannelPayload]: @@ -1213,7 +1213,7 @@ def _create_channel( self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, category: Optional[Snowflake] = None, **options: Any, ) -> Coroutine[Any, Any, GuildChannelPayload]: @@ -1253,7 +1253,7 @@ async def create_text_channel( topic: str = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, ) -> TextChannel: @@ -1395,7 +1395,7 @@ async def create_voice_channel( user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> VoiceChannel: """|coro| @@ -1488,7 +1488,7 @@ async def create_stage_channel( user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> StageChannel: """|coro| @@ -1581,7 +1581,7 @@ async def create_category( self, name: str, *, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, position: int = MISSING, ) -> CategoryChannel: @@ -1636,7 +1636,7 @@ async def create_forum( category: Optional[CategoryChannel] = None, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, From a13fc8f835a94fced4f0b5ceb7c9004e07fb879e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 15 Jul 2024 20:24:35 -0400 Subject: [PATCH 080/354] Add support for guild_banner and display_banner --- discord/asset.py | 11 +++++++++++ discord/member.py | 24 ++++++++++++++++++++++++ discord/types/member.py | 1 + 3 files changed, 36 insertions(+) diff --git a/discord/asset.py b/discord/asset.py index 7b9b711334a3..e3422f3110d4 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -246,6 +246,17 @@ def _from_guild_avatar(cls, state: _State, guild_id: int, member_id: int, avatar animated=animated, ) + @classmethod + def _from_guild_banner(cls, state: _State, guild_id: int, member_id: int, banner: str) -> Self: + animated = banner.startswith('a_') + format = 'gif' if animated else 'png' + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024", + key=banner, + animated=animated, + ) + @classmethod def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self: return cls( diff --git a/discord/member.py b/discord/member.py index 74ba8693259c..5b9b03f9e499 100644 --- a/discord/member.py +++ b/discord/member.py @@ -322,6 +322,7 @@ class Member(discord.abc.Messageable, _UserTag): '_user', '_state', '_avatar', + '_banner', '_flags', '_avatar_decoration_data', ) @@ -358,6 +359,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) self._avatar: Optional[str] = data.get('avatar') + self._banner: Optional[str] = data.get('banner') self._permissions: Optional[int] self._flags: int = data['flags'] self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') @@ -649,6 +651,28 @@ def guild_avatar(self) -> Optional[Asset]: return None return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar) + @property + def display_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the member's displayed banner, if any. + + This is the member's guild banner if available, otherwise it's their + global banner. If the member has no banner set then ``None`` is returned. + + .. versionadded:: 2.5 + """ + return self.guild_banner or self._user.banner + + @property + def guild_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner + the member has. If unavailable, ``None`` is returned. + + .. versionadded:: 2.5 + """ + if self._banner is None: + return None + return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) + @property def activity(self) -> Optional[ActivityTypes]: """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary diff --git a/discord/types/member.py b/discord/types/member.py index 6968edb6f47f..88fb619fd398 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -48,6 +48,7 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + banner: NotRequired[Optional[str]] avatar_decoration_data: NotRequired[AvatarDecorationData] From 86dd29a8e8ceeb9db4086a12b73a79b07ef04aef Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 16 Jul 2024 03:03:50 -0400 Subject: [PATCH 081/354] Properly copy banner information on updates --- discord/member.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/member.py b/discord/member.py index 5b9b03f9e499..2eadacd27331 100644 --- a/discord/member.py +++ b/discord/member.py @@ -440,6 +440,7 @@ def _copy(cls, member: Self) -> Self: self._permissions = member._permissions self._state = member._state self._avatar = member._avatar + self._banner = member._banner self._avatar_decoration_data = member._avatar_decoration_data # Reference will not be copied unless necessary by PRESENCE_UPDATE @@ -468,6 +469,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self.timed_out_until = utils.parse_time(data.get('communication_disabled_until')) self._roles = utils.SnowflakeList(map(int, data['roles'])) self._avatar = data.get('avatar') + self._banner = data.get('banner') self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') From 04b2e494f72f282c3ca7b730d66cb2086226b9be Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 21 Jul 2024 18:44:07 -0400 Subject: [PATCH 082/354] Fix documentation for abc.GuildChannel.move to be more clear --- discord/abc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index fec57b52af98..7f10811c4394 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1130,10 +1130,10 @@ async def move(self, **kwargs: Any) -> None: channel list (or category if given). This is mutually exclusive with ``beginning``, ``before``, and ``after``. before: :class:`~discord.abc.Snowflake` - The channel that should be before our current channel. + Whether to move the channel before the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``after``. after: :class:`~discord.abc.Snowflake` - The channel that should be after our current channel. + Whether to move the channel after the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``before``. offset: :class:`int` The number of channels to offset the move by. For example, From ff638d393d0f5a83639ccc087bec9bf588b59a22 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:15:38 +0200 Subject: [PATCH 083/354] Fix Polls limiting duration at 7 days --- discord/poll.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/poll.py b/discord/poll.py index c523f16098f7..7dc3897ac0f1 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -388,9 +388,6 @@ def _from_data(cls, *, data: PollPayload, message: Message, state: ConnectionSta # self.created_at = message.created_at # duration = self.created_at - expiry - if (duration.total_seconds() / 3600) > 168: # As the duration may exceed little milliseconds then we fix it - duration = datetime.timedelta(days=7) - self = cls( duration=duration, multiple=multiselect, From a183a56dd98dd14f49b9229578664a91ed0daa9e Mon Sep 17 00:00:00 2001 From: Pipythonmc <47196755+pythonmcpi@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:11:12 -0700 Subject: [PATCH 084/354] Fix _get_command_error improperly handling some error messages --- discord/app_commands/errors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index dc63f10e8c88..87a5dbb59d46 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -485,6 +485,10 @@ def _get_command_error( if key == 'options': for index, d in remaining.items(): _get_command_error(index, d, children, messages, indent=indent + 2) + elif key == '_errors': + errors = [x.get('message', '') for x in remaining] + + messages.extend(f'{indentation} {message}' for message in errors) else: if isinstance(remaining, dict): try: @@ -493,8 +497,6 @@ def _get_command_error( errors = _flatten_error_dict(remaining, key=key) else: errors = {key: ' '.join(x.get('message', '') for x in inner_errors)} - else: - errors = _flatten_error_dict(remaining, key=key) messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) From c41cadfa91a2e490c3d42cdf19412cbcef190f3c Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 15:22:35 -0400 Subject: [PATCH 085/354] Fix introduced potential TypeError with _get_command_error --- discord/app_commands/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index 87a5dbb59d46..abdc9f2f01a4 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -498,7 +498,8 @@ def _get_command_error( else: errors = {key: ' '.join(x.get('message', '') for x in inner_errors)} - messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) + if isinstance(errors, dict): + messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) class CommandSyncFailure(AppCommandError, HTTPException): From da89fbc8b5b6f0cb994d8575c04d4219b38c13a6 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 28 Aug 2024 15:23:51 -0400 Subject: [PATCH 086/354] Re-add client connector param This provides paths for users to handle two entirely seperate issues - Alternative fix for #9870 - Allows handling of windows sslcontext issues without a global truststore.inject_into_ssl() use --- discord/client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/discord/client.py b/discord/client.py index a91be7160d20..4f5dfe9f099d 100644 --- a/discord/client.py +++ b/discord/client.py @@ -249,6 +249,11 @@ class Client: set to is ``30.0`` seconds. .. versionadded:: 2.0 + connector: Optional[:class:`aiohttp.BaseConnector`] + The aiohhtp connector to use for this client. This can be used to control underlying aiohttp + behavior, such as setting a dns resolver or sslcontext. + + .. versionadded:: 2.5 Attributes ----------- @@ -264,6 +269,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: self.shard_id: Optional[int] = options.get('shard_id') self.shard_count: Optional[int] = options.get('shard_count') + connector: Optional[aiohttp.BaseConnector] = options.get('connector', None) proxy: Optional[str] = options.pop('proxy', None) proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None) unsync_clock: bool = options.pop('assume_unsync_clock', True) @@ -271,6 +277,7 @@ def __init__(self, *, intents: Intents, **options: Any) -> None: max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None) self.http: HTTPClient = HTTPClient( self.loop, + connector, proxy=proxy, proxy_auth=proxy_auth, unsync_clock=unsync_clock, From 38bbfed7174bccf5490e5f931b3d8ba8e15922b9 Mon Sep 17 00:00:00 2001 From: ambdroid <61042504+ambdroid@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:31:38 -0400 Subject: [PATCH 087/354] Fix Poll.duration rounding error --- discord/poll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/poll.py b/discord/poll.py index 7dc3897ac0f1..88ed5b534d2f 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -384,9 +384,9 @@ def _from_data(cls, *, data: PollPayload, message: Message, state: ConnectionSta question_data = data.get('question') question = question_data.get('text') expiry = utils.parse_time(data['expiry']) # If obtained via API, then expiry is set. - duration = expiry - message.created_at + # expiry - message.created_at may be a few nanos away from the actual duration + duration = datetime.timedelta(hours=round((expiry - message.created_at).total_seconds() / 3600)) # self.created_at = message.created_at - # duration = self.created_at - expiry self = cls( duration=duration, From a6d1dc04555ef0395c26e11c5822164c49f805fd Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:32:20 +0200 Subject: [PATCH 088/354] Add support for getting the attachment's title --- discord/message.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/message.py b/discord/message.py index 1d1a3c96c19c..0247ee8c11c8 100644 --- a/discord/message.py +++ b/discord/message.py @@ -194,6 +194,10 @@ class Attachment(Hashable): The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -211,6 +215,7 @@ class Attachment(Hashable): 'duration', 'waveform', '_flags', + 'title', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -226,6 +231,7 @@ def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.description: Optional[str] = data.get('description') self.ephemeral: bool = data.get('ephemeral', False) self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') waveform = data.get('waveform') self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None From 0a2faa6f5dce2a06973d22c7193040d8127cfae7 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:04:33 +0530 Subject: [PATCH 089/354] Fix default_avatar for team user and webhook --- discord/user.py | 2 +- discord/webhook/async_.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/discord/user.py b/discord/user.py index 5151957dc998..c5391372aa58 100644 --- a/discord/user.py +++ b/discord/user.py @@ -171,7 +171,7 @@ def avatar(self) -> Optional[Asset]: @property def default_avatar(self) -> Asset: """:class:`Asset`: Returns the default avatar for a given user.""" - if self.discriminator == '0': + if self.discriminator in ('0', '0000'): avatar_id = (self.id >> 22) % len(DefaultAvatar) else: avatar_id = int(self.discriminator) % 5 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 3b0416f9a72c..91b021a5e958 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -38,7 +38,7 @@ from .. import utils from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError from ..message import Message -from ..enums import try_enum, WebhookType, ChannelType +from ..enums import try_enum, WebhookType, ChannelType, DefaultAvatar from ..user import BaseUser, User from ..flags import MessageFlags from ..asset import Asset @@ -360,7 +360,7 @@ def edit_webhook_message( multipart: Optional[List[Dict[str, Any]]] = None, files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, - ) -> Response[Message]: + ) -> Response[MessagePayload]: route = Route( 'PATCH', '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', @@ -1049,12 +1049,11 @@ def avatar(self) -> Optional[Asset]: @property def default_avatar(self) -> Asset: """ - :class:`Asset`: Returns the default avatar. This is always the blurple avatar. + :class:`Asset`: Returns the default avatar. .. versionadded:: 2.0 """ - # Default is always blurple apparently - return Asset._from_default_avatar(self._state, 0) + return Asset._from_default_avatar(self._state, (self.id >> 22) % len(DefaultAvatar)) @property def display_avatar(self) -> Asset: From 1c6f3c5ff1f8ac0fd0f49280a0309d656c918b33 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:37:32 +0200 Subject: [PATCH 090/354] [docs] Remove pack_id attribute from Sticker [docs] Remove unnecessary pack_id --- discord/sticker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/sticker.py b/discord/sticker.py index 30eb62c70e23..96deef475a8b 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -258,8 +258,6 @@ class Sticker(_StickerTag): The id of the sticker. description: :class:`str` The description of the sticker. - pack_id: :class:`int` - The id of the sticker's pack. format: :class:`StickerFormatType` The format for the sticker's image. url: :class:`str` From 7cf6df166d522867ee73426662db99eb370cf2c1 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Thu, 29 Aug 2024 01:18:19 +0530 Subject: [PATCH 091/354] Fix url for GIF StickerItem --- discord/sticker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/sticker.py b/discord/sticker.py index 96deef475a8b..ebaf1534c0de 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -203,7 +203,10 @@ def __init__(self, *, state: ConnectionState, data: StickerItemPayload) -> None: self.name: str = data['name'] self.id: int = int(data['id']) self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type']) - self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' + if self.format is StickerFormatType.gif: + self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif' + else: + self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' def __repr__(self) -> str: return f'' From 794f2bf149d8849a28acadefe0dac871417a6bfc Mon Sep 17 00:00:00 2001 From: fretgfr <51489753+fretgfr@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:48:38 -0400 Subject: [PATCH 092/354] [docs] correct hyperlink to discord docs --- discord/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/embeds.py b/discord/embeds.py index 6a79fef71c62..2071ef9db8ee 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -199,7 +199,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: """Converts a :class:`dict` to a :class:`Embed` provided it is in the format that Discord expects it to be in. - You can find out about this format in the :ddocs:`official Discord documentation `. + You can find out about this format in the :ddocs:`official Discord documentation `. Parameters ----------- From 8104ff2ad472ced922bd2dd14d34ab01f40aa014 Mon Sep 17 00:00:00 2001 From: lmaotrigine <57328245+lmaotrigine@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:20:51 +0530 Subject: [PATCH 093/354] [docs] Fix typehint for Embed.set_(image,thumbnail) --- discord/embeds.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index 2071ef9db8ee..8b2cecb91ca6 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -413,8 +413,9 @@ def set_image(self, *, url: Optional[Any]) -> Self: Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the image. Only HTTP(S) is supported. + If ``None`` is passed, any existing image is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ @@ -457,8 +458,9 @@ def set_thumbnail(self, *, url: Optional[Any]) -> Self: Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the thumbnail. Only HTTP(S) is supported. + If ``None`` is passed, any existing thumbnail is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ From 3018fee4432c5718fe90aff61cfe4b7932b95f60 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 15:51:17 -0400 Subject: [PATCH 094/354] Remove stale documentation in Embed.set_thumbnail --- discord/embeds.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index 8b2cecb91ca6..258ef0dfd86c 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -453,9 +453,6 @@ def set_thumbnail(self, *, url: Optional[Any]) -> Self: This function returns the class instance to allow for fluent-style chaining. - .. versionchanged:: 1.4 - Passing ``None`` removes the thumbnail. - Parameters ----------- url: Optional[:class:`str`] From 9ab938a9ea5ffcd13c8a0d8fbc662ef1a651ca5a Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:54:34 +0200 Subject: [PATCH 095/354] Add Guild.fetch_role --- discord/guild.py | 31 +++++++++++++++++++++++++++++++ discord/http.py | 3 +++ 2 files changed, 34 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index f34818b63503..5296faa07c53 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3408,6 +3408,37 @@ async def fetch_roles(self) -> List[Role]: data = await self._state.http.get_roles(self.id) return [Role(guild=self, state=self._state, data=d) for d in data] + async def fetch_role(self, role_id: int, /) -> Role: + """|coro| + + Retrieves a :class:`Role` with the specified ID. + + .. versionadded:: 2.5 + + .. note:: + + This method is an API call. For general usage, consider :attr:`get_role` instead. + + Parameters + ---------- + role_id: :class:`int` + The role's ID. + + Raises + ------- + NotFound + The role requested could not be found. + HTTPException + An error occurred fetching the role. + + Returns + ------- + :class:`Role` + The retrieved role. + """ + data = await self._state.http.get_role(self.id, role_id) + return Role(guild=self, state=self._state, data=data) + @overload async def create_role( self, diff --git a/discord/http.py b/discord/http.py index 608595fe3b89..08daa6efc1db 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1858,6 +1858,9 @@ def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Resp def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]: return self.request(Route('GET', '/guilds/{guild_id}/roles', guild_id=guild_id)) + def get_role(self, guild_id: Snowflake, role_id: Snowflake) -> Response[role.Role]: + return self.request(Route('GET', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id)) + def edit_role( self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any ) -> Response[role.Role]: From d578709640356fd6195df4a8cfd80d9638a6ec52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 28 Aug 2024 21:00:27 +0100 Subject: [PATCH 096/354] Add approximate_user_install_count to AppInfo --- discord/appinfo.py | 6 ++++++ discord/types/appinfo.py | 1 + 2 files changed, 7 insertions(+) diff --git a/discord/appinfo.py b/discord/appinfo.py index 074892d051af..932f852c2d79 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -147,6 +147,10 @@ class AppInfo: The approximate count of the guilds the bot was added to. .. versionadded:: 2.4 + approximate_user_install_count: Optional[:class:`int`] + The approximate count of the user-level installations the bot has. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -175,6 +179,7 @@ class AppInfo: 'interactions_endpoint_url', 'redirect_uris', 'approximate_guild_count', + 'approximate_user_install_count', ) def __init__(self, state: ConnectionState, data: AppInfoPayload): @@ -212,6 +217,7 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url') self.redirect_uris: List[str] = data.get('redirect_uris', []) self.approximate_guild_count: int = data.get('approximate_guild_count', 0) + self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count') def __repr__(self) -> str: return ( diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index e291babfa3e0..ae7fc7e0df21 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -45,6 +45,7 @@ class BaseAppInfo(TypedDict): summary: str description: str flags: int + approximate_user_install_count: NotRequired[int] cover_image: NotRequired[str] terms_of_service_url: NotRequired[str] privacy_policy_url: NotRequired[str] From f9dbe60fc41771c3914fef1a6f86f7c14ca61ddc Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 28 Aug 2024 16:05:39 -0400 Subject: [PATCH 097/354] Revert "Set socket family of connector to AF_INET" This change was made since Discord doesn't support IPv6, and there were concerns about clients with DNS64 enabled without NAT64. However, this breaks hosts who don't have v4 connectivity and are _actually_ running NAT64. Having DNS64 without NAT64 is really an issue on the client's end. It would break far more than just discord.py, so I don't think we should be concerned about those cases. --- discord/http.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index 08daa6efc1db..a16c036296c8 100644 --- a/discord/http.py +++ b/discord/http.py @@ -48,7 +48,6 @@ from urllib.parse import quote as _uriquote from collections import deque import datetime -import socket import aiohttp @@ -798,8 +797,7 @@ async def close(self) -> None: async def static_login(self, token: str) -> user.User: # Necessary to get aiohttp to stop complaining about session creation if self.connector is MISSING: - # discord does not support ipv6 - self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET) + self.connector = aiohttp.TCPConnector(limit=0) self.__session = aiohttp.ClientSession( connector=self.connector, From 34bf026a0213902f821173858cf80d2ab067e51a Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:15:26 +0200 Subject: [PATCH 098/354] Add support for get sticker pack --- discord/client.py | 27 +++++++++++++++++++++++++++ discord/http.py | 3 +++ discord/sticker.py | 18 +++++++----------- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/discord/client.py b/discord/client.py index 4f5dfe9f099d..601dbbaf40cf 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2926,6 +2926,33 @@ async def fetch_premium_sticker_packs(self) -> List[StickerPack]: data = await self.http.list_premium_sticker_packs() return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']] + async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPack: + """|coro| + + Retrieves a premium sticker pack with the specified ID. + + .. versionadded:: 2.5 + + Parameters + ---------- + sticker_pack_id: :class:`int` + The sticker pack's ID to fetch from. + + Raises + ------- + NotFound + A sticker pack with this ID does not exist. + HTTPException + Retrieving the sticker pack failed. + + Returns + ------- + :class:`.StickerPack` + The retrieved premium sticker pack. + """ + data = await self.http.get_sticker_pack(sticker_pack_id) + return StickerPack(state=self._connection, data=data) + async def create_dm(self, user: Snowflake) -> DMChannel: """|coro| diff --git a/discord/http.py b/discord/http.py index a16c036296c8..d7e33b560de6 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1609,6 +1609,9 @@ def estimate_pruned_members( def get_sticker(self, sticker_id: Snowflake) -> Response[sticker.Sticker]: return self.request(Route('GET', '/stickers/{sticker_id}', sticker_id=sticker_id)) + def get_sticker_pack(self, sticker_pack_id: Snowflake) -> Response[sticker.StickerPack]: + return self.request(Route('GET', '/sticker-packs/{sticker_pack_id}', sticker_pack_id=sticker_pack_id)) + def list_premium_sticker_packs(self) -> Response[sticker.ListPremiumStickerPacks]: return self.request(Route('GET', '/sticker-packs')) diff --git a/discord/sticker.py b/discord/sticker.py index ebaf1534c0de..bf90f8866526 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -28,8 +28,7 @@ from .mixins import Hashable from .asset import Asset, AssetMixin -from .utils import cached_slot_property, find, snowflake_time, get, MISSING, _get_as_snowflake -from .errors import InvalidData +from .utils import cached_slot_property, snowflake_time, get, MISSING, _get_as_snowflake from .enums import StickerType, StickerFormatType, try_enum __all__ = ( @@ -51,7 +50,6 @@ Sticker as StickerPayload, StandardSticker as StandardStickerPayload, GuildSticker as GuildStickerPayload, - ListPremiumStickerPacks as ListPremiumStickerPacksPayload, ) @@ -353,9 +351,12 @@ async def pack(self) -> StickerPack: Retrieves the sticker pack that this sticker belongs to. + .. versionchanged:: 2.5 + Now raises ``NotFound`` instead of ``InvalidData``. + Raises -------- - InvalidData + NotFound The corresponding sticker pack was not found. HTTPException Retrieving the sticker pack failed. @@ -365,13 +366,8 @@ async def pack(self) -> StickerPack: :class:`StickerPack` The retrieved sticker pack. """ - data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs() - packs = data['sticker_packs'] - pack = find(lambda d: int(d['id']) == self.pack_id, packs) - - if pack: - return StickerPack(state=self._state, data=pack) - raise InvalidData(f'Could not find corresponding sticker pack for {self!r}') + data = await self._state.http.get_sticker_pack(self.pack_id) + return StickerPack(state=self._state, data=data) class GuildSticker(Sticker): From aeab0d48fd7e6483a977995450515e69e2b30cdb Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 28 Aug 2024 22:15:58 +0200 Subject: [PATCH 099/354] Fix stacklevel for Message.interaction deprecation warning --- discord/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/utils.py b/discord/utils.py index 99c7cfc94233..89cc8bdebfce 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -302,7 +302,7 @@ def decorated(*args: P.args, **kwargs: P.kwargs) -> T: else: fmt = '{0.__name__} is deprecated.' - warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning) + warnings.warn(fmt.format(func, instead), stacklevel=2, category=DeprecationWarning) warnings.simplefilter('default', DeprecationWarning) # reset filter return func(*args, **kwargs) From 624b5b7643ce6dfc783aa9ce365026af41f78e4b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 16:17:03 -0400 Subject: [PATCH 100/354] Use fallback audioop package for Python v3.13 or higher Fix #9477 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 046084ebb6d9..ef2b6c534f42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ aiohttp>=3.7.4,<4 +audioop-lts; python_version>='3.13' From 643a7f4e1d351cbe4928ed4915474a1295ebb480 Mon Sep 17 00:00:00 2001 From: Deep Jain Date: Wed, 28 Aug 2024 16:21:41 -0400 Subject: [PATCH 101/354] Add DummyCookieJar to client owned ClientSession --- discord/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/http.py b/discord/http.py index d7e33b560de6..494ecef0c7e7 100644 --- a/discord/http.py +++ b/discord/http.py @@ -803,6 +803,7 @@ async def static_login(self, token: str) -> user.User: connector=self.connector, ws_response_class=DiscordClientWebSocketResponse, trace_configs=None if self.http_trace is None else [self.http_trace], + cookie_jar=aiohttp.DummyCookieJar(), ) self._global_over = asyncio.Event() self._global_over.set() From fde7131d26bfa431a476a8a64ea3ae9f18bf2683 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:30:18 +0200 Subject: [PATCH 102/354] Add missing guild incident fields Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/guild.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 5296faa07c53..07a072d1f6bd 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4435,6 +4435,28 @@ def dms_paused_until(self) -> Optional[datetime.datetime]: return utils.parse_time(self._incidents_data.get('dms_disabled_until')) + @property + def dm_spam_detected_at(self) -> Optional[datetime.datetime]: + """:class:`datetime.datetime`: Returns the time when DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('dm_spam_detected_at')) + + @property + def raid_detected_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('raid_detected_at')) + def invites_paused(self) -> bool: """:class:`bool`: Whether invites are paused in the guild. @@ -4451,6 +4473,26 @@ def dms_paused(self) -> bool: .. versionadded:: 2.4 """ if not self.dms_paused_until: - return False + return 'INVITES_DISABLED' in self.features return self.dms_paused_until > utils.utcnow() + + def is_dm_spam_detected(self) -> bool: + """:class:`bool`: Whether DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.dm_spam_detected_at: + return False + + return self.dm_spam_detected_at > utils.utcnow() + + def is_raid_detected(self) -> bool: + """:class:`bool`: Whether a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.raid_detected_at: + return False + + return self.raid_detected_at > utils.utcnow() From 62e52803a74de3471594c7ac294af3926f51edec Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:36:22 +0200 Subject: [PATCH 103/354] Add support Member.fetch_voice --- discord/http.py | 7 +++++++ discord/member.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/discord/http.py b/discord/http.py index 494ecef0c7e7..e1bb04483076 100644 --- a/discord/http.py +++ b/discord/http.py @@ -92,6 +92,7 @@ welcome_screen, sku, poll, + voice, ) from .types.snowflake import Snowflake, SnowflakeList @@ -1147,6 +1148,12 @@ def edit_member( r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, json=fields, reason=reason) + def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)) + + def get_voice_state(self, guild_id: Snowflake, user_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)) + # Channel management def edit_channel( diff --git a/discord/member.py b/discord/member.py index 2eadacd27331..66c7715721d9 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1153,6 +1153,40 @@ async def remove_roles(self, *roles: Snowflake, reason: Optional[str] = None, at for role in roles: await req(guild_id, user_id, role.id, reason=reason) + async def fetch_voice(self) -> VoiceState: + """|coro| + + Retrieves the current voice state from this member. + + .. versionadded:: 2.5 + + Raises + ------- + NotFound + The member is not in a voice channel. + Forbidden + You do not have permissions to get a voice state. + HTTPException + Retrieving the voice state failed. + + Returns + ------- + :class:`VoiceState` + The current voice state of the member. + """ + guild_id = self.guild.id + if self._state.self_id == self.id: + data = await self._state.http.get_my_voice_state(guild_id) + else: + data = await self._state.http.get_voice_state(guild_id, self.id) + + channel_id = data.get('channel_id') + channel: Optional[VocalGuildChannel] = None + if channel_id is not None: + channel = self.guild.get_channel(int(channel_id)) # type: ignore # must be voice channel here + + return VoiceState(data=data, channel=channel) + def get_role(self, role_id: int, /) -> Optional[Role]: """Returns a role with the given ID from roles which the member has. From 463b4bd570faba36f1d8eac1ae29413bce3da9a6 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:15:15 +0200 Subject: [PATCH 104/354] Add support for application emojis Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/client.py | 102 ++++++++++++++++++++++++++++++++++++++- discord/emoji.py | 49 ++++++++++++++++++- discord/http.py | 57 +++++++++++++++++++++- discord/types/appinfo.py | 5 ++ 4 files changed, 209 insertions(+), 4 deletions(-) diff --git a/discord/client.py b/discord/client.py index 601dbbaf40cf..1d2c2a326fcc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -366,7 +366,13 @@ def guilds(self) -> Sequence[Guild]: @property def emojis(self) -> Sequence[Emoji]: - """Sequence[:class:`.Emoji`]: The emojis that the connected client has.""" + """Sequence[:class:`.Emoji`]: The emojis that the connected client has. + + .. note:: + + This not include the emojis that are owned by the application. + Use :meth:`.fetch_application_emoji` to get those. + """ return self._connection.emojis @property @@ -3073,3 +3079,97 @@ def persistent_views(self) -> Sequence[View]: .. versionadded:: 2.0 """ return self._connection.persistent_views + + async def create_application_emoji( + self, + *, + name: str, + image: bytes, + ) -> Emoji: + """|coro| + + Create an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The emoji name. Must be at least 2 characters. + image: :class:`bytes` + The :term:`py:bytes-like object` representing the image data to use. + Only JPG, PNG and GIF images are supported. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Creating the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji that was created. + """ + if self.application_id is None: + raise MissingApplicationID + + img = utils._bytes_to_base64_data(image) + data = await self.http.create_application_emoji(self.application_id, name, img) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emoji(self, emoji_id: int, /) -> Emoji: + """|coro| + + Retrieves an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + emoji_id: :class:`int` + The emoji ID to retrieve. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji requested. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emoji(self.application_id, emoji_id) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emojis(self) -> List[Emoji]: + """|coro| + + Retrieves all emojis for the current application. + + .. versionadded:: 2.5 + + Raises + ------- + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emojis failed. + + Returns + ------- + List[:class:`.Emoji`] + The list of emojis for the current application. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emojis(self.application_id) + return [Emoji(guild=Object(0), state=self._connection, data=emoji) for emoji in data['items']] diff --git a/discord/emoji.py b/discord/emoji.py index 045486d5a8a3..e011495fd9cc 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -29,6 +29,8 @@ from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag, PartialEmoji from .user import User +from .app_commands.errors import MissingApplicationID +from .object import Object # fmt: off __all__ = ( @@ -93,6 +95,10 @@ class Emoji(_EmojiTag, AssetMixin): user: Optional[:class:`User`] The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and having :attr:`~Permissions.manage_emojis`. + + Or if :meth:`.is_application_owned` is ``True``, this is the team member that uploaded + the emoji, or the bot user if it was uploaded using the API and this can + only be retrieved using :meth:`~discord.Client.fetch_application_emoji` or :meth:`~discord.Client.fetch_application_emojis`. """ __slots__: Tuple[str, ...] = ( @@ -108,7 +114,7 @@ class Emoji(_EmojiTag, AssetMixin): 'available', ) - def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload) -> None: + def __init__(self, *, guild: Snowflake, state: ConnectionState, data: EmojiPayload) -> None: self.guild_id: int = guild.id self._state: ConnectionState = state self._from_data(data) @@ -196,20 +202,32 @@ async def delete(self, *, reason: Optional[str] = None) -> None: Deletes the custom emoji. - You must have :attr:`~Permissions.manage_emojis` to do this. + You must have :attr:`~Permissions.manage_emojis` to do this if + :meth:`.is_application_owned` is ``False``. Parameters ----------- reason: Optional[:class:`str`] The reason for deleting this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to delete emojis. HTTPException An error occurred deleting the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing. """ + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + await self._state.http.delete_application_emoji(application_id, self.id) + return await self._state.http.delete_custom_emoji(self.guild_id, self.id, reason=reason) @@ -231,15 +249,22 @@ async def edit( The new emoji name. roles: List[:class:`~discord.abc.Snowflake`] A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. + + This does not apply if :meth:`.is_application_owned` is ``True``. + reason: Optional[:class:`str`] The reason for editing this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to edit emojis. HTTPException An error occurred editing the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing Returns -------- @@ -253,5 +278,25 @@ async def edit( if roles is not MISSING: payload['roles'] = [role.id for role in roles] + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + payload.pop('roles', None) + data = await self._state.http.edit_application_emoji( + application_id, + self.id, + payload=payload, + ) + return Emoji(guild=Object(0), data=data, state=self._state) + data = await self._state.http.edit_custom_emoji(self.guild_id, self.id, payload=payload, reason=reason) return Emoji(guild=self.guild, data=data, state=self._state) # type: ignore # if guild is None, the http request would have failed + + def is_application_owned(self) -> bool: + """:class:`bool`: Whether the emoji is owned by an application. + + .. versionadded:: 2.5 + """ + return self.guild_id == 0 diff --git a/discord/http.py b/discord/http.py index e1bb04483076..6230f9b1da16 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2515,7 +2515,7 @@ def delete_entitlement(self, application_id: Snowflake, entitlement_id: Snowflak ), ) - # Misc + # Application def application_info(self) -> Response[appinfo.AppInfo]: return self.request(Route('GET', '/oauth2/applications/@me')) @@ -2536,6 +2536,59 @@ def edit_application_info(self, *, reason: Optional[str], payload: Any) -> Respo payload = {k: v for k, v in payload.items() if k in valid_keys} return self.request(Route('PATCH', '/applications/@me'), json=payload, reason=reason) + def get_application_emojis(self, application_id: Snowflake) -> Response[appinfo.ListAppEmojis]: + return self.request(Route('GET', '/applications/{application_id}/emojis', application_id=application_id)) + + def get_application_emoji(self, application_id: Snowflake, emoji_id: Snowflake) -> Response[emoji.Emoji]: + return self.request( + Route( + 'GET', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + ) + + def create_application_emoji( + self, + application_id: Snowflake, + name: str, + image: str, + ) -> Response[emoji.Emoji]: + payload = { + 'name': name, + 'image': image, + } + + return self.request( + Route('POST', '/applications/{application_id}/emojis', application_id=application_id), json=payload + ) + + def edit_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + *, + payload: Dict[str, Any], + ) -> Response[emoji.Emoji]: + r = Route( + 'PATCH', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + return self.request(r, json=payload) + + def delete_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + ) -> Response[None]: + return self.request( + Route( + 'DELETE', + '/applications/{application_id}/emojis/{emoji_id}', + application_id=application_id, + emoji_id=emoji_id, + ) + ) + + # Poll + def get_poll_answer_voters( self, channel_id: Snowflake, @@ -2573,6 +2626,8 @@ def end_poll(self, channel_id: Snowflake, message_id: Snowflake) -> Response[mes ) ) + # Misc + async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: try: data = await self.request(Route('GET', '/gateway')) diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index ae7fc7e0df21..7cca955b7986 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -30,6 +30,7 @@ from .user import User from .team import Team from .snowflake import Snowflake +from .emoji import Emoji class InstallParams(TypedDict): @@ -79,3 +80,7 @@ class PartialAppInfo(BaseAppInfo, total=False): class GatewayAppInfo(TypedDict): id: Snowflake flags: int + + +class ListAppEmojis(TypedDict): + items: List[Emoji] From a08f7a14fff66e0bc532fca15275ee8de0f542ac Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 29 Aug 2024 03:04:14 -0400 Subject: [PATCH 105/354] Add a warning if interaction endpoint URL is set on login --- discord/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/client.py b/discord/client.py index 1d2c2a326fcc..50f76d5a279e 100644 --- a/discord/client.py +++ b/discord/client.py @@ -636,6 +636,11 @@ async def login(self, token: str) -> None: if self._connection.application_id is None: self._connection.application_id = self._application.id + if self._application.interactions_endpoint_url is not None: + _log.warning( + 'Application has an interaction endpoint URL set, this means registered components and app commands will not be received by the library.' + ) + if not self._connection.application_flags: self._connection.application_flags = self._application.flags From 6d8198126a34b33b38a7a32391065f8dd5e2fab7 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 29 Aug 2024 03:05:35 -0400 Subject: [PATCH 106/354] Remove aiodns from being used on Windows --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d36c85c82741..596e6ef0874d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ docs = [ ] speed = [ "orjson>=3.5.4", - "aiodns>=1.1", + "aiodns>=1.1; sys_platform != 'win32'", "Brotli", "cchardet==2.1.7; python_version < '3.10'", ] From d3e63a0162a7a0db3b6d822e596c4745af875c2c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:38:00 +0530 Subject: [PATCH 107/354] Fix Guild.invites_paused method --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 07a072d1f6bd..9bdcda129b11 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4463,7 +4463,7 @@ def invites_paused(self) -> bool: .. versionadded:: 2.4 """ if not self.invites_paused_until: - return False + return 'INVITES_DISABLED' in self.features return self.invites_paused_until > utils.utcnow() @@ -4473,7 +4473,7 @@ def dms_paused(self) -> bool: .. versionadded:: 2.4 """ if not self.dms_paused_until: - return 'INVITES_DISABLED' in self.features + return False return self.dms_paused_until > utils.utcnow() From dee5bf65c63fc2feca00d32e5f4a9fa293636291 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 29 Aug 2024 16:15:14 -0400 Subject: [PATCH 108/354] Update MemberFlags to have newest values --- discord/flags.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/discord/flags.py b/discord/flags.py index 3d31e3a58a0c..583f98c347eb 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -2032,6 +2032,48 @@ def started_onboarding(self): """:class:`bool`: Returns ``True`` if the member has started onboarding.""" return 1 << 3 + @flag_value + def guest(self): + """:class:`bool`: Returns ``True`` if the member is a guest and can only access + the voice channel they were invited to. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def started_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has started Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 5 + + @flag_value + def completed_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has completed Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 6 + + @flag_value + def automod_quarantined_username(self): + """:class:`bool`: Returns ``True`` if the member's username, nickname, or global name has been + blocked by AutoMod. + + .. versionadded:: 2.5 + """ + return 1 << 7 + + @flag_value + def dm_settings_upsell_acknowledged(self): + """:class:`bool`: Returns ``True`` if the member has dismissed the DM settings upsell. + + .. versionadded:: 2.5 + """ + return 1 << 9 + @fill_with_flags() class AttachmentFlags(BaseFlags): From 733c583b7250f34b657986f834f8fc7c461fcf15 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:29:05 -0400 Subject: [PATCH 109/354] Remove _get_poll lookup in Message constructor This was triggering a terrible performance regression for no good reason for all created messages that didn't have a poll, which is essentially 99.99% of messages leading to MESSAGE_CREATE dispatches having degraded performance. --- discord/message.py | 2 +- discord/state.py | 6 ------ discord/webhook/async_.py | 5 ----- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/discord/message.py b/discord/message.py index 0247ee8c11c8..2feeff2a6357 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1849,7 +1849,7 @@ def __init__( try: self.poll = Poll._from_data(data=data['poll'], message=self, state=state) except KeyError: - self.poll = state._get_poll(self.id) + pass try: # if the channel doesn't have a guild attribute, we handle that diff --git a/discord/state.py b/discord/state.py index db0395266b6e..6279f14bf952 100644 --- a/discord/state.py +++ b/discord/state.py @@ -510,12 +510,6 @@ def _remove_private_channel(self, channel: PrivateChannel) -> None: def _get_message(self, msg_id: Optional[int]) -> Optional[Message]: return utils.find(lambda m: m.id == msg_id, reversed(self._messages)) if self._messages else None - def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: - message = self._get_message(msg_id) - if not message: - return - return message.poll - def _add_guild_from_data(self, data: GuildPayload) -> Guild: guild = Guild(data=data, state=self) self._add_guild(guild) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 91b021a5e958..2d9856ae3d0e 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -721,11 +721,6 @@ def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]: return self._parent._get_guild(guild_id) return None - def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: - if self._parent is not None: - return self._parent._get_poll(msg_id) - return None - def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser: if self._parent is not None: return self._parent.store_user(data, cache=cache) From 66d74054ddd0552505a705a8af850b16ec5ea0a5 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:32:52 -0400 Subject: [PATCH 110/354] Remove outdated leftover comment about polls --- discord/message.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index 2feeff2a6357..76127f869e48 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1843,8 +1843,6 @@ def __init__( self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] - # This updates the poll so it has the counts, if the message - # was previously cached. self.poll: Optional[Poll] = None try: self.poll = Poll._from_data(data=data['poll'], message=self, state=state) From df4b1c88df741b439e97049e5c92feb969bdd5d3 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:51:56 -0400 Subject: [PATCH 111/354] Move MissingApplicationID to top-level discord namespace --- discord/__init__.py | 6 ++++++ discord/app_commands/errors.py | 20 +------------------- discord/client.py | 2 +- discord/emoji.py | 2 +- discord/errors.py | 25 +++++++++++++++++++++++++ discord/sku.py | 2 +- docs/api.rst | 3 +++ docs/interactions/api.rst | 5 +---- 8 files changed, 39 insertions(+), 26 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 765719b68731..2cf64c93470d 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -84,4 +84,10 @@ class VersionInfo(NamedTuple): logging.getLogger(__name__).addHandler(logging.NullHandler()) +# This is a backwards compatibility hack and should be removed in v3 +# Essentially forcing the exception to have different base classes +# In the future, this should only inherit from ClientException +if len(MissingApplicationID.__bases__) == 1: + MissingApplicationID.__bases__ = (app_commands.AppCommandError, ClientException) + del logging, NamedTuple, Literal, VersionInfo diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index abdc9f2f01a4..2efb4e5b008b 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -27,7 +27,7 @@ from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union from ..enums import AppCommandOptionType, AppCommandType, Locale -from ..errors import DiscordException, HTTPException, _flatten_error_dict +from ..errors import DiscordException, HTTPException, _flatten_error_dict, MissingApplicationID as MissingApplicationID from ..utils import _human_join __all__ = ( @@ -59,11 +59,6 @@ CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu] -APP_ID_NOT_FOUND = ( - 'Client does not have an application_id set. Either the function was called before on_ready ' - 'was called or application_id was not passed to the Client constructor.' -) - class AppCommandError(DiscordException): """The base exception type for all application command related errors. @@ -422,19 +417,6 @@ def __init__(self, command: Union[Command[Any, ..., Any], ContextMenu, Group]): super().__init__(msg) -class MissingApplicationID(AppCommandError): - """An exception raised when the client does not have an application ID set. - An application ID is required for syncing application commands. - - This inherits from :exc:`~discord.app_commands.AppCommandError`. - - .. versionadded:: 2.0 - """ - - def __init__(self, message: Optional[str] = None): - super().__init__(message or APP_ID_NOT_FOUND) - - def _get_command_error( index: str, inner: Any, diff --git a/discord/client.py b/discord/client.py index 50f76d5a279e..2ca8c2ae0c6b 100644 --- a/discord/client.py +++ b/discord/client.py @@ -84,7 +84,7 @@ from typing_extensions import Self from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime - from .app_commands import Command, ContextMenu, MissingApplicationID + from .app_commands import Command, ContextMenu from .automod import AutoModAction, AutoModRule from .channel import DMChannel, GroupChannel from .ext.commands import AutoShardedBot, Bot, Context, CommandError diff --git a/discord/emoji.py b/discord/emoji.py index e011495fd9cc..74f344acc1d4 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -29,7 +29,7 @@ from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag, PartialEmoji from .user import User -from .app_commands.errors import MissingApplicationID +from .errors import MissingApplicationID from .object import Object # fmt: off diff --git a/discord/errors.py b/discord/errors.py index 6035ace7c20f..a40842578d0d 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -47,6 +47,12 @@ 'ConnectionClosed', 'PrivilegedIntentsRequired', 'InteractionResponded', + 'MissingApplicationID', +) + +APP_ID_NOT_FOUND = ( + 'Client does not have an application_id set. Either the function was called before on_ready ' + 'was called or application_id was not passed to the Client constructor.' ) @@ -278,3 +284,22 @@ class InteractionResponded(ClientException): def __init__(self, interaction: Interaction): self.interaction: Interaction = interaction super().__init__('This interaction has already been responded to before') + + +class MissingApplicationID(ClientException): + """An exception raised when the client does not have an application ID set. + + An application ID is required for syncing application commands and various + other application tasks such as SKUs or application emojis. + + This inherits from :exc:`~discord.app_commands.AppCommandError` + and :class:`~discord.ClientException`. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.5 + This is now exported to the ``discord`` namespace and now inherits from :class:`~discord.ClientException`. + """ + + def __init__(self, message: Optional[str] = None): + super().__init__(message or APP_ID_NOT_FOUND) diff --git a/discord/sku.py b/discord/sku.py index 2af171c1d97a..e8780399ca6a 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -28,7 +28,7 @@ from typing import Optional, TYPE_CHECKING from . import utils -from .app_commands import MissingApplicationID +from .errors import MissingApplicationID from .enums import try_enum, SKUType, EntitlementType from .flags import SKUFlags diff --git a/docs/api.rst b/docs/api.rst index 41cf6549d169..e415ea8ceb07 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5440,6 +5440,8 @@ The following exceptions are thrown by the library. .. autoexception:: InteractionResponded +.. autoexception:: MissingApplicationID + .. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusNotLoaded @@ -5457,6 +5459,7 @@ Exception Hierarchy - :exc:`ConnectionClosed` - :exc:`PrivilegedIntentsRequired` - :exc:`InteractionResponded` + - :exc:`MissingApplicationID` - :exc:`GatewayNotFound` - :exc:`HTTPException` - :exc:`Forbidden` diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 211cd790f6b5..aeb6a25c613d 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -872,9 +872,6 @@ Exceptions .. autoexception:: discord.app_commands.CommandNotFound :members: -.. autoexception:: discord.app_commands.MissingApplicationID - :members: - .. autoexception:: discord.app_commands.CommandSyncFailure :members: @@ -899,7 +896,7 @@ Exception Hierarchy - :exc:`~discord.app_commands.CommandAlreadyRegistered` - :exc:`~discord.app_commands.CommandSignatureMismatch` - :exc:`~discord.app_commands.CommandNotFound` - - :exc:`~discord.app_commands.MissingApplicationID` + - :exc:`~discord.MissingApplicationID` - :exc:`~discord.app_commands.CommandSyncFailure` - :exc:`~discord.HTTPException` - :exc:`~discord.app_commands.CommandSyncFailure` From 59f877fcf013c4ddeeb2b39fc21f03e76f995461 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 2 Sep 2024 10:53:31 -0400 Subject: [PATCH 112/354] Fix and add test for missing discord.Permission bits --- discord/permissions.py | 2 +- tests/test_permissions_all.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/test_permissions_all.py diff --git a/discord/permissions.py b/discord/permissions.py index 17c7b38c95dc..b553e2578161 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -187,7 +187,7 @@ def all(cls) -> Self: permissions set to ``True``. """ # Some of these are 0 because we don't want to set unnecessary bits - return cls(0b0000_0000_0000_0010_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: diff --git a/tests/test_permissions_all.py b/tests/test_permissions_all.py new file mode 100644 index 000000000000..883dc1b630ba --- /dev/null +++ b/tests/test_permissions_all.py @@ -0,0 +1,7 @@ +import discord + +from functools import reduce +from operator import or_ + +def test_permissions_all(): + assert discord.Permissions.all().value == reduce(or_, discord.Permissions.VALID_FLAGS.values()) From a70217a719c95984d084385fc438761d23294117 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:05:17 +0200 Subject: [PATCH 113/354] Support for Soundboard and VC effects --- discord/__init__.py | 1 + discord/audit_logs.py | 6 + discord/channel.py | 163 +++++++++++++++++- discord/client.py | 46 +++++ discord/enums.py | 12 ++ discord/flags.py | 24 ++- discord/gateway.py | 26 +-- discord/guild.py | 172 +++++++++++++++++++ discord/http.py | 73 ++++++++ discord/soundboard.py | 325 ++++++++++++++++++++++++++++++++++++ discord/state.py | 82 +++++++++ discord/types/audit_log.py | 13 ++ discord/types/channel.py | 15 ++ discord/types/emoji.py | 2 + discord/types/gateway.py | 12 +- discord/types/guild.py | 4 + discord/types/soundboard.py | 49 ++++++ discord/utils.py | 14 +- docs/api.rst | 172 ++++++++++++++++++- 19 files changed, 1185 insertions(+), 26 deletions(-) create mode 100644 discord/soundboard.py create mode 100644 discord/types/soundboard.py diff --git a/discord/__init__.py b/discord/__init__.py index 2cf64c93470d..780460dc51f6 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -70,6 +70,7 @@ from .threads import * from .automod import * from .poll import * +from .soundboard import * class VersionInfo(NamedTuple): diff --git a/discord/audit_logs.py b/discord/audit_logs.py index fc1bc298b602..59d563829257 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -235,6 +235,10 @@ def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAc return [AutoModRuleAction.from_data(action) for action in data] +def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji: + return PartialEmoji(name=data) + + E = TypeVar('E', bound=enums.Enum) @@ -341,6 +345,8 @@ class AuditLogChanges: 'available_tags': (None, _transform_forum_tags), 'flags': (None, _transform_overloaded_flags), 'default_reaction_emoji': (None, _transform_default_reaction), + 'emoji_name': ('emoji', _transform_default_emoji), + 'user_id': ('user', _transform_member_id) } # fmt: on diff --git a/discord/channel.py b/discord/channel.py index 55b25a03c4ca..37f0ab6fa718 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -47,7 +47,16 @@ import discord.abc from .scheduled_event import ScheduledEvent from .permissions import PermissionOverwrite, Permissions -from .enums import ChannelType, ForumLayoutType, ForumOrderType, PrivacyLevel, try_enum, VideoQualityMode, EntityType +from .enums import ( + ChannelType, + ForumLayoutType, + ForumOrderType, + PrivacyLevel, + try_enum, + VideoQualityMode, + EntityType, + VoiceChannelEffectAnimationType, +) from .mixins import Hashable from . import utils from .utils import MISSING @@ -58,6 +67,8 @@ from .partial_emoji import _EmojiTag, PartialEmoji from .flags import ChannelFlags from .http import handle_message_parameters +from .object import Object +from .soundboard import BaseSoundboardSound, SoundboardDefaultSound __all__ = ( 'TextChannel', @@ -69,6 +80,8 @@ 'ForumChannel', 'GroupChannel', 'PartialMessageable', + 'VoiceChannelEffect', + 'VoiceChannelSoundEffect', ) if TYPE_CHECKING: @@ -76,7 +89,6 @@ from .types.threads import ThreadArchiveDuration from .role import Role - from .object import Object from .member import Member, VoiceState from .abc import Snowflake, SnowflakeTime from .embeds import Embed @@ -100,8 +112,11 @@ ForumChannel as ForumChannelPayload, MediaChannel as MediaChannelPayload, ForumTag as ForumTagPayload, + VoiceChannelEffect as VoiceChannelEffectPayload, ) from .types.snowflake import SnowflakeList + from .types.soundboard import BaseSoundboardSound as BaseSoundboardSoundPayload + from .soundboard import SoundboardSound OverwriteKeyT = TypeVar('OverwriteKeyT', Role, BaseUser, Object, Union[Role, Member, Object]) @@ -111,6 +126,121 @@ class ThreadWithMessage(NamedTuple): message: Message +class VoiceChannelEffectAnimation(NamedTuple): + id: int + type: VoiceChannelEffectAnimationType + + +class VoiceChannelSoundEffect(BaseSoundboardSound): + """Represents a Discord voice channel sound effect. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sound effects are equal. + + .. describe:: x != y + + Checks if two sound effects are not equal. + + .. describe:: hash(x) + + Returns the sound effect's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + """ + + __slots__ = ('_state',) + + def __init__(self, *, state: ConnectionState, id: int, volume: float): + data: BaseSoundboardSoundPayload = { + 'sound_id': id, + 'volume': volume, + } + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} volume={self.volume}>" + + @property + def created_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the snowflake's creation time in UTC. + Returns ``None`` if it's a default sound.""" + if self.is_default(): + return None + else: + return utils.snowflake_time(self.id) + + def is_default(self) -> bool: + """:class:`bool`: Whether it's a default sound or not.""" + # if it's smaller than the Discord Epoch it cannot be a snowflake + return self.id < utils.DISCORD_EPOCH + + +class VoiceChannelEffect: + """Represents a Discord voice channel effect. + + .. versionadded:: 2.5 + + Attributes + ------------ + channel: :class:`VoiceChannel` + The channel in which the effect is sent. + user: Optional[:class:`Member`] + The user who sent the effect. ``None`` if not found in cache. + animation: Optional[:class:`VoiceChannelEffectAnimation`] + The animation the effect has. Returns ``None`` if the effect has no animation. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the effect. + sound: Optional[:class:`VoiceChannelSoundEffect`] + The sound of the effect. Returns ``None`` if it's an emoji effect. + """ + + __slots__ = ('channel', 'user', 'animation', 'emoji', 'sound') + + def __init__(self, *, state: ConnectionState, data: VoiceChannelEffectPayload, guild: Guild): + self.channel: VoiceChannel = guild.get_channel(int(data['channel_id'])) # type: ignore # will always be a VoiceChannel + self.user: Optional[Member] = guild.get_member(int(data['user_id'])) + self.animation: Optional[VoiceChannelEffectAnimation] = None + + animation_id = data.get('animation_id') + if animation_id is not None: + animation_type = try_enum(VoiceChannelEffectAnimationType, data['animation_type']) # type: ignore # cannot be None here + self.animation = VoiceChannelEffectAnimation(id=animation_id, type=animation_type) + + emoji = data.get('emoji') + self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict(emoji) if emoji is not None else None + self.sound: Optional[VoiceChannelSoundEffect] = None + + sound_id: Optional[int] = utils._get_as_snowflake(data, 'sound_id') + if sound_id is not None: + sound_volume = data.get('sound_volume') or 0.0 + self.sound = VoiceChannelSoundEffect(state=state, id=sound_id, volume=sound_volume) + + def __repr__(self) -> str: + attrs = [ + ('channel', self.channel), + ('user', self.user), + ('animation', self.animation), + ('emoji', self.emoji), + ('sound', self.sound), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + def is_sound(self) -> bool: + """:class:`bool`: Whether the effect is a sound or not.""" + return self.sound is not None + + class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. @@ -1456,6 +1586,35 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona # the payload will always be the proper channel payload return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + async def send_sound(self, sound: Union[SoundboardSound, SoundboardDefaultSound], /) -> None: + """|coro| + + Sends a soundboard sound for this channel. + + You must have :attr:`~Permissions.speak` and :attr:`~Permissions.use_soundboard` to do this. + Additionally, you must have :attr:`~Permissions.use_external_sounds` if the sound is from + a different guild. + + .. versionadded:: 2.5 + + Parameters + ----------- + sound: Union[:class:`SoundboardSound`, :class:`SoundboardDefaultSound`] + The sound to send for this channel. + + Raises + ------- + Forbidden + You do not have permissions to send a sound for this channel. + HTTPException + Sending the sound failed. + """ + payload = {'sound_id': sound.id} + if not isinstance(sound, SoundboardDefaultSound) and self.guild.id != sound.guild.id: + payload['source_guild_id'] = sound.guild.id + + await self._state.http.send_soundboard_sound(self.id, **payload) + class StageChannel(VocalGuildChannel): """Represents a Discord guild stage channel. diff --git a/discord/client.py b/discord/client.py index 2ca8c2ae0c6b..30c3a1c6f9c6 100644 --- a/discord/client.py +++ b/discord/client.py @@ -77,6 +77,7 @@ from .stage_instance import StageInstance from .threads import Thread from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory +from .soundboard import SoundboardDefaultSound, SoundboardSound if TYPE_CHECKING: from types import TracebackType @@ -383,6 +384,14 @@ def stickers(self) -> Sequence[GuildSticker]: """ return self._connection.stickers + @property + def soundboard_sounds(self) -> List[SoundboardSound]: + """List[:class:`.SoundboardSound`]: The soundboard sounds that the connected client has. + + .. versionadded:: 2.5 + """ + return self._connection.soundboard_sounds + @property def cached_messages(self) -> Sequence[Message]: """Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached. @@ -1109,6 +1118,23 @@ def get_sticker(self, id: int, /) -> Optional[GuildSticker]: """ return self._connection.get_sticker(id) + def get_soundboard_sound(self, id: int, /) -> Optional[SoundboardSound]: + """Returns a soundboard sound with the given ID. + + .. versionadded:: 2.5 + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + -------- + Optional[:class:`.SoundboardSound`] + The soundboard sound or ``None`` if not found. + """ + return self._connection.get_soundboard_sound(id) + def get_all_channels(self) -> Generator[GuildChannel, None, None]: """A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'. @@ -2964,6 +2990,26 @@ async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPa data = await self.http.get_sticker_pack(sticker_pack_id) return StickerPack(state=self._connection, data=data) + async def fetch_soundboard_default_sounds(self) -> List[SoundboardDefaultSound]: + """|coro| + + Retrieves all default soundboard sounds. + + .. versionadded:: 2.5 + + Raises + ------- + HTTPException + Retrieving the default soundboard sounds failed. + + Returns + --------- + List[:class:`.SoundboardDefaultSound`] + All default soundboard sounds. + """ + data = await self.http.get_soundboard_default_sounds() + return [SoundboardDefaultSound(state=self._connection, data=sound) for sound in data] + async def create_dm(self, user: Snowflake) -> DMChannel: """|coro| diff --git a/discord/enums.py b/discord/enums.py index eaf8aef5e058..9000c8c04576 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -74,6 +74,7 @@ 'EntitlementType', 'EntitlementOwnerType', 'PollLayoutType', + 'VoiceChannelEffectAnimationType', ) @@ -377,6 +378,9 @@ class AuditLogAction(Enum): thread_update = 111 thread_delete = 112 app_command_permission_update = 121 + soundboard_sound_create = 130 + soundboard_sound_update = 131 + soundboard_sound_delete = 132 automod_rule_create = 140 automod_rule_update = 141 automod_rule_delete = 142 @@ -447,6 +451,9 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.automod_timeout_member: None, AuditLogAction.creator_monetization_request_created: None, AuditLogAction.creator_monetization_terms_accepted: None, + AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create, + AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update, + AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete, } # fmt: on return lookup[self] @@ -835,6 +842,11 @@ class ReactionType(Enum): burst = 1 +class VoiceChannelEffectAnimationType(Enum): + premium = 0 + basic = 1 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/flags.py b/discord/flags.py index 583f98c347eb..abe77f3c2670 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -871,34 +871,52 @@ def bans(self): @alias_flag_value def emojis(self): - """:class:`bool`: Alias of :attr:`.emojis_and_stickers`. + """:class:`bool`: Alias of :attr:`.expressions`. .. versionchanged:: 2.0 Changed to an alias. """ return 1 << 3 - @flag_value + @alias_flag_value def emojis_and_stickers(self): - """:class:`bool`: Whether guild emoji and sticker related events are enabled. + """:class:`bool`: Alias of :attr:`.expressions`. .. versionadded:: 2.0 + .. versionchanged:: 2.5 + Changed to an alias. + """ + return 1 << 3 + + @flag_value + def expressions(self): + """:class:`bool`: Whether guild emoji, sticker, and soundboard sound related events are enabled. + + .. versionadded:: 2.5 + This corresponds to the following events: - :func:`on_guild_emojis_update` - :func:`on_guild_stickers_update` + - :func:`on_soundboard_sound_create` + - :func:`on_soundboard_sound_update` + - :func:`on_soundboard_sound_delete` This also corresponds to the following attributes and classes in terms of cache: - :class:`Emoji` - :class:`GuildSticker` + - :class:`SoundboardSound` - :meth:`Client.get_emoji` - :meth:`Client.get_sticker` + - :meth:`Client.get_soundboard_sound` - :meth:`Client.emojis` - :meth:`Client.stickers` + - :meth:`Client.soundboard_sounds` - :attr:`Guild.emojis` - :attr:`Guild.stickers` + - :attr:`Guild.soundboard_sounds` """ return 1 << 3 diff --git a/discord/gateway.py b/discord/gateway.py index b8936bf5708b..e6fb7d8bfac5 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -295,19 +295,19 @@ class DiscordWebSocket: # fmt: off DEFAULT_GATEWAY = yarl.URL('wss://gateway.discord.gg/') - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE = 3 - VOICE_STATE = 4 - VOICE_PING = 5 - RESUME = 6 - RECONNECT = 7 - REQUEST_MEMBERS = 8 - INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE = 3 + VOICE_STATE = 4 + VOICE_PING = 5 + RESUME = 6 + RECONNECT = 7 + REQUEST_MEMBERS = 8 + INVALIDATE_SESSION = 9 + HELLO = 10 + HEARTBEAT_ACK = 11 + GUILD_SYNC = 12 # fmt: on def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None: diff --git a/discord/guild.py b/discord/guild.py index 9bdcda129b11..5fd60a853fa6 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -94,6 +94,7 @@ from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji +from .soundboard import SoundboardSound __all__ = ( @@ -328,6 +329,7 @@ class Guild(Hashable): '_safety_alerts_channel_id', 'max_stage_video_users', '_incidents_data', + '_soundboard_sounds', ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { @@ -345,6 +347,7 @@ def __init__(self, *, data: GuildPayload, state: ConnectionState) -> None: self._threads: Dict[int, Thread] = {} self._stage_instances: Dict[int, StageInstance] = {} self._scheduled_events: Dict[int, ScheduledEvent] = {} + self._soundboard_sounds: Dict[int, SoundboardSound] = {} self._state: ConnectionState = state self._member_count: Optional[int] = None self._from_data(data) @@ -390,6 +393,12 @@ def _filter_threads(self, channel_ids: Set[int]) -> Dict[int, Thread]: del self._threads[k] return to_remove + def _add_soundboard_sound(self, sound: SoundboardSound, /) -> None: + self._soundboard_sounds[sound.id] = sound + + def _remove_soundboard_sound(self, sound: SoundboardSound, /) -> None: + self._soundboard_sounds.pop(sound.id, None) + def __str__(self) -> str: return self.name or '' @@ -547,6 +556,11 @@ def _from_data(self, guild: GuildPayload) -> None: scheduled_event = ScheduledEvent(data=s, state=self._state) self._scheduled_events[scheduled_event.id] = scheduled_event + if 'soundboard_sounds' in guild: + for s in guild['soundboard_sounds']: + soundboard_sound = SoundboardSound(guild=self, data=s, state=self._state) + self._add_soundboard_sound(soundboard_sound) + @property def channels(self) -> Sequence[GuildChannel]: """Sequence[:class:`abc.GuildChannel`]: A list of channels that belongs to this guild.""" @@ -996,6 +1010,37 @@ def get_scheduled_event(self, scheduled_event_id: int, /) -> Optional[ScheduledE """ return self._scheduled_events.get(scheduled_event_id) + @property + def soundboard_sounds(self) -> Sequence[SoundboardSound]: + """Sequence[:class:`SoundboardSound`]: Returns a sequence of the guild's soundboard sounds. + + .. versionadded:: 2.5 + """ + return utils.SequenceProxy(self._soundboard_sounds.values()) + + def get_soundboard_sound(self, sound_id: int, /) -> Optional[SoundboardSound]: + """Returns a soundboard sound with the given ID. + + .. versionadded:: 2.5 + + Parameters + ----------- + sound_id: :class:`int` + The ID to search for. + + Returns + -------- + Optional[:class:`SoundboardSound`] + The soundboard sound or ``None`` if not found. + """ + return self._soundboard_sounds.get(sound_id) + + def _resolve_soundboard_sound(self, id: Optional[int], /) -> Optional[SoundboardSound]: + if id is None: + return + + return self._soundboard_sounds.get(id) + @property def owner(self) -> Optional[Member]: """Optional[:class:`Member`]: The member that owns the guild.""" @@ -4496,3 +4541,130 @@ def is_raid_detected(self) -> bool: return False return self.raid_detected_at > utils.utcnow() + + async def fetch_soundboard_sound(self, sound_id: int, /) -> SoundboardSound: + """|coro| + + Retrieves a :class:`SoundboardSound` with the specified ID. + + .. versionadded:: 2.5 + + .. note:: + + Using this, in order to receive :attr:`SoundboardSound.user`, you must have :attr:`~Permissions.create_expressions` + or :attr:`~Permissions.manage_expressions`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`get_soundboard_sound` instead. + + Raises + ------- + NotFound + The sound requested could not be found. + HTTPException + Retrieving the sound failed. + + Returns + -------- + :class:`SoundboardSound` + The retrieved sound. + """ + data = await self._state.http.get_soundboard_sound(self.id, sound_id) + return SoundboardSound(guild=self, state=self._state, data=data) + + async def fetch_soundboard_sounds(self) -> List[SoundboardSound]: + """|coro| + + Retrieves a list of all soundboard sounds for the guild. + + .. versionadded:: 2.5 + + .. note:: + + Using this, in order to receive :attr:`SoundboardSound.user`, you must have :attr:`~Permissions.create_expressions` + or :attr:`~Permissions.manage_expressions`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`soundboard_sounds` instead. + + Raises + ------- + HTTPException + Retrieving the sounds failed. + + Returns + -------- + List[:class:`SoundboardSound`] + The retrieved soundboard sounds. + """ + data = await self._state.http.get_soundboard_sounds(self.id) + return [SoundboardSound(guild=self, state=self._state, data=sound) for sound in data['items']] + + async def create_soundboard_sound( + self, + *, + name: str, + sound: bytes, + volume: float = 1, + emoji: Optional[EmojiInputType] = None, + reason: Optional[str] = None, + ) -> SoundboardSound: + """|coro| + + Creates a :class:`SoundboardSound` for the guild. + You must have :attr:`Permissions.create_expressions` to do this. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The name of the sound. Must be between 2 and 32 characters. + sound: :class:`bytes` + The :term:`py:bytes-like object` representing the sound data. + Only MP3 and OGG sound files that don't exceed the duration of 5.2s are supported. + volume: :class:`float` + The volume of the sound. Must be between 0 and 1. Defaults to ``1``. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The emoji of the sound. + reason: Optional[:class:`str`] + The reason for creating the sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to create a soundboard sound. + HTTPException + Creating the soundboard sound failed. + + Returns + ------- + :class:`SoundboardSound` + The newly created soundboard sound. + """ + payload: Dict[str, Any] = { + 'name': name, + 'sound': utils._bytes_to_base64_data(sound, audio=True), + 'volume': volume, + 'emoji_id': None, + 'emoji_name': None, + } + + if emoji is not None: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + else: + partial_emoji = None + + if partial_emoji is not None: + if partial_emoji.id is None: + payload['emoji_name'] = partial_emoji.name + else: + payload['emoji_id'] = partial_emoji.id + + data = await self._state.http.create_soundboard_sound(self.id, reason=reason, **payload) + return SoundboardSound(guild=self, state=self._state, data=data) diff --git a/discord/http.py b/discord/http.py index 6230f9b1da16..24605c4fcd9c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -93,6 +93,7 @@ sku, poll, voice, + soundboard, ) from .types.snowflake import Snowflake, SnowflakeList @@ -2515,6 +2516,78 @@ def delete_entitlement(self, application_id: Snowflake, entitlement_id: Snowflak ), ) + # Soundboard + + def get_soundboard_default_sounds(self) -> Response[List[soundboard.SoundboardDefaultSound]]: + return self.request(Route('GET', '/soundboard-default-sounds')) + + def get_soundboard_sound(self, guild_id: Snowflake, sound_id: Snowflake) -> Response[soundboard.SoundboardSound]: + return self.request( + Route('GET', '/guilds/{guild_id}/soundboard-sounds/{sound_id}', guild_id=guild_id, sound_id=sound_id) + ) + + def get_soundboard_sounds(self, guild_id: Snowflake) -> Response[Dict[str, List[soundboard.SoundboardSound]]]: + return self.request(Route('GET', '/guilds/{guild_id}/soundboard-sounds', guild_id=guild_id)) + + def create_soundboard_sound( + self, guild_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[soundboard.SoundboardSound]: + valid_keys = ( + 'name', + 'sound', + 'volume', + 'emoji_id', + 'emoji_name', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} + + return self.request( + Route('POST', '/guilds/{guild_id}/soundboard-sounds', guild_id=guild_id), json=payload, reason=reason + ) + + def edit_soundboard_sound( + self, guild_id: Snowflake, sound_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[soundboard.SoundboardSound]: + valid_keys = ( + 'name', + 'volume', + 'emoji_id', + 'emoji_name', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys} + + return self.request( + Route( + 'PATCH', + '/guilds/{guild_id}/soundboard-sounds/{sound_id}', + guild_id=guild_id, + sound_id=sound_id, + ), + json=payload, + reason=reason, + ) + + def delete_soundboard_sound(self, guild_id: Snowflake, sound_id: Snowflake, *, reason: Optional[str]) -> Response[None]: + return self.request( + Route( + 'DELETE', + '/guilds/{guild_id}/soundboard-sounds/{sound_id}', + guild_id=guild_id, + sound_id=sound_id, + ), + reason=reason, + ) + + def send_soundboard_sound(self, channel_id: Snowflake, **payload: Any) -> Response[None]: + valid_keys = ('sound_id', 'source_guild_id') + payload = {k: v for k, v in payload.items() if k in valid_keys} + print(payload) + return self.request( + (Route('POST', '/channels/{channel_id}/send-soundboard-sound', channel_id=channel_id)), json=payload + ) + # Application def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/soundboard.py b/discord/soundboard.py new file mode 100644 index 000000000000..3351aacb78ff --- /dev/null +++ b/discord/soundboard.py @@ -0,0 +1,325 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from . import utils +from .mixins import Hashable +from .partial_emoji import PartialEmoji, _EmojiTag +from .user import User +from .utils import MISSING +from .asset import Asset, AssetMixin + +if TYPE_CHECKING: + import datetime + from typing import Dict, Any + + from .types.soundboard import ( + BaseSoundboardSound as BaseSoundboardSoundPayload, + SoundboardDefaultSound as SoundboardDefaultSoundPayload, + SoundboardSound as SoundboardSoundPayload, + ) + from .state import ConnectionState + from .guild import Guild + from .message import EmojiInputType + +__all__ = ('BaseSoundboardSound', 'SoundboardDefaultSound', 'SoundboardSound') + + +class BaseSoundboardSound(Hashable, AssetMixin): + """Represents a generic Discord soundboard sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + """ + + __slots__ = ('_state', 'id', 'volume') + + def __init__(self, *, state: ConnectionState, data: BaseSoundboardSoundPayload): + self._state: ConnectionState = state + self.id: int = int(data['sound_id']) + self._update(data) + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.id == other.id + return NotImplemented + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def _update(self, data: BaseSoundboardSoundPayload): + self.volume: float = data['volume'] + + @property + def url(self) -> str: + """:class:`str`: Returns the URL of the sound.""" + return f'{Asset.BASE}/soundboard-sounds/{self.id}' + + +class SoundboardDefaultSound(BaseSoundboardSound): + """Represents a Discord soundboard default sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + name: :class:`str` + The name of the sound. + emoji: :class:`PartialEmoji` + The emoji of the sound. + """ + + __slots__ = ('name', 'emoji') + + def __init__(self, *, state: ConnectionState, data: SoundboardDefaultSoundPayload): + self.name: str = data['name'] + self.emoji: PartialEmoji = PartialEmoji(name=data['emoji_name']) + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + attrs = [ + ('id', self.id), + ('name', self.name), + ('volume', self.volume), + ('emoji', self.emoji), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + +class SoundboardSound(BaseSoundboardSound): + """Represents a Discord soundboard sound. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two sounds are equal. + + .. describe:: x != y + + Checks if two sounds are not equal. + + .. describe:: hash(x) + + Returns the sound's hash. + + Attributes + ------------ + id: :class:`int` + The ID of the sound. + volume: :class:`float` + The volume of the sound as floating point percentage (e.g. ``1.0`` for 100%). + name: :class:`str` + The name of the sound. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the sound. ``None`` if no emoji is set. + guild: :class:`Guild` + The guild in which the sound is uploaded. + available: :class:`bool` + Whether this sound is available for use. + """ + + __slots__ = ('_state', 'name', 'emoji', '_user', 'available', '_user_id', 'guild') + + def __init__(self, *, guild: Guild, state: ConnectionState, data: SoundboardSoundPayload): + super().__init__(state=state, data=data) + self.guild = guild + self._user_id = utils._get_as_snowflake(data, 'user_id') + self._user = data.get('user') + + self._update(data) + + def __repr__(self) -> str: + attrs = [ + ('id', self.id), + ('name', self.name), + ('volume', self.volume), + ('emoji', self.emoji), + ('user', self.user), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f"<{self.__class__.__name__} {inner}>" + + def _update(self, data: SoundboardSoundPayload): + super()._update(data) + + self.name: str = data['name'] + self.emoji: Optional[PartialEmoji] = None + + emoji_id = utils._get_as_snowflake(data, 'emoji_id') + emoji_name = data['emoji_name'] + if emoji_id is not None or emoji_name is not None: + self.emoji = PartialEmoji(id=emoji_id, name=emoji_name) # type: ignore # emoji_name cannot be None here + + self.available: bool = data['available'] + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the snowflake's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user who uploaded the sound.""" + if self._user is None: + if self._user_id is None: + return None + return self._state.get_user(self._user_id) + return User(state=self._state, data=self._user) + + async def edit( + self, + *, + name: str = MISSING, + volume: Optional[float] = MISSING, + emoji: Optional[EmojiInputType] = MISSING, + reason: Optional[str] = None, + ): + """|coro| + + Edits the soundboard sound. + + You must have :attr:`~Permissions.manage_expressions` to edit the sound. + If the sound was created by the client, you must have either :attr:`~Permissions.manage_expressions` + or :attr:`~Permissions.create_expressions`. + + Parameters + ---------- + name: :class:`str` + The new name of the sound. Must be between 2 and 32 characters. + volume: Optional[:class:`float`] + The new volume of the sound. Must be between 0 and 1. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The new emoji of the sound. + reason: Optional[:class:`str`] + The reason for editing this sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to edit the soundboard sound. + HTTPException + Editing the soundboard sound failed. + + Returns + ------- + :class:`SoundboardSound` + The newly updated soundboard sound. + """ + + payload: Dict[str, Any] = {} + + if name is not MISSING: + payload['name'] = name + + if volume is not MISSING: + payload['volume'] = volume + + if emoji is not MISSING: + if emoji is None: + payload['emoji_id'] = None + payload['emoji_name'] = None + else: + if isinstance(emoji, _EmojiTag): + partial_emoji = emoji._to_partial() + elif isinstance(emoji, str): + partial_emoji = PartialEmoji.from_str(emoji) + else: + partial_emoji = None + + if partial_emoji is not None: + if partial_emoji.id is None: + payload['emoji_name'] = partial_emoji.name + else: + payload['emoji_id'] = partial_emoji.id + + data = await self._state.http.edit_soundboard_sound(self.guild.id, self.id, reason=reason, **payload) + return SoundboardSound(guild=self.guild, state=self._state, data=data) + + async def delete(self, *, reason: Optional[str] = None) -> None: + """|coro| + + Deletes the soundboard sound. + + You must have :attr:`~Permissions.manage_expressions` to delete the sound. + If the sound was created by the client, you must have either :attr:`~Permissions.manage_expressions` + or :attr:`~Permissions.create_expressions`. + + Parameters + ----------- + reason: Optional[:class:`str`] + The reason for deleting this sound. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to delete the soundboard sound. + HTTPException + Deleting the soundboard sound failed. + """ + await self._state.http.delete_soundboard_sound(self.guild.id, self.id, reason=reason) diff --git a/discord/state.py b/discord/state.py index 6279f14bf952..91fd915bf6bc 100644 --- a/discord/state.py +++ b/discord/state.py @@ -78,6 +78,7 @@ from .automod import AutoModRule, AutoModAction from .audit_logs import AuditLogEntry from ._types import ClientT +from .soundboard import SoundboardSound if TYPE_CHECKING: from .abc import PrivateChannel @@ -455,6 +456,14 @@ def emojis(self) -> Sequence[Emoji]: def stickers(self) -> Sequence[GuildSticker]: return utils.SequenceProxy(self._stickers.values()) + @property + def soundboard_sounds(self) -> List[SoundboardSound]: + all_sounds = [] + for guild in self.guilds: + all_sounds.extend(guild.soundboard_sounds) + + return all_sounds + def get_emoji(self, emoji_id: Optional[int]) -> Optional[Emoji]: # the keys of self._emojis are ints return self._emojis.get(emoji_id) # type: ignore @@ -1555,6 +1564,62 @@ def parse_guild_scheduled_event_user_remove(self, data: gw.GuildScheduledEventUs else: _log.debug('SCHEDULED_EVENT_USER_REMOVE referencing unknown guild ID: %s. Discarding.', data['guild_id']) + def parse_guild_soundboard_sound_create(self, data: gw.GuildSoundBoardSoundCreateEvent) -> None: + guild_id = int(data['guild_id']) # type: ignore # can't be None here + guild = self._get_guild(guild_id) + if guild is not None: + sound = SoundboardSound(guild=guild, state=self, data=data) + guild._add_soundboard_sound(sound) + self.dispatch('soundboard_sound_create', sound) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_CREATE referencing unknown guild ID: %s. Discarding.', guild_id) + + def _update_and_dispatch_sound_update(self, sound: SoundboardSound, data: gw.GuildSoundBoardSoundUpdateEvent): + old_sound = copy.copy(sound) + sound._update(data) + self.dispatch('soundboard_sound_update', old_sound, sound) + + def parse_guild_soundboard_sound_update(self, data: gw.GuildSoundBoardSoundUpdateEvent) -> None: + guild_id = int(data['guild_id']) # type: ignore # can't be None here + guild = self._get_guild(guild_id) + if guild is not None: + sound_id = int(data['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + self._update_and_dispatch_sound_update(sound, data) + else: + _log.warning('GUILD_SOUNDBOARD_SOUND_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + + def parse_guild_soundboard_sound_delete(self, data: gw.GuildSoundBoardSoundDeleteEvent) -> None: + guild_id = int(data['guild_id']) + guild = self._get_guild(guild_id) + if guild is not None: + sound_id = int(data['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + guild._remove_soundboard_sound(sound) + self.dispatch('soundboard_sound_delete', sound) + else: + _log.warning('GUILD_SOUNDBOARD_SOUND_DELETE referencing unknown sound ID: %s. Discarding.', sound_id) + else: + _log.debug('GUILD_SOUNDBOARD_SOUND_DELETE referencing unknown guild ID: %s. Discarding.', guild_id) + + def parse_guild_soundboard_sounds_update(self, data: gw.GuildSoundBoardSoundsUpdateEvent) -> None: + for raw_sound in data: + guild_id = int(raw_sound['guild_id']) # type: ignore # can't be None here + guild = self._get_guild(guild_id) + if guild is not None: + sound_id = int(raw_sound['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + self._update_and_dispatch_sound_update(sound, raw_sound) + else: + _log.warning('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) + else: + _log.debug('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + def parse_application_command_permissions_update(self, data: GuildApplicationCommandPermissionsPayload): raw = RawAppCommandPermissionsUpdateEvent(data=data, state=self) self.dispatch('raw_app_command_permissions_update', raw) @@ -1585,6 +1650,14 @@ def parse_voice_state_update(self, data: gw.VoiceStateUpdateEvent) -> None: else: _log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id']) + def parse_voice_channel_effect_send(self, data: gw.VoiceChannelEffectSendEvent): + guild = self._get_guild(int(data['guild_id'])) + if guild is not None: + effect = VoiceChannelEffect(state=self, data=data, guild=guild) + self.dispatch('voice_channel_effect', effect) + else: + _log.debug('VOICE_CHANNEL_EFFECT_SEND referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + def parse_voice_server_update(self, data: gw.VoiceServerUpdateEvent) -> None: key_id = int(data['guild_id']) @@ -1707,6 +1780,15 @@ def get_channel(self, id: Optional[int]) -> Optional[Union[Channel, Thread]]: def create_message(self, *, channel: MessageableChannel, data: MessagePayload) -> Message: return Message(state=self, channel=channel, data=data) + def get_soundboard_sound(self, id: Optional[int]) -> Optional[SoundboardSound]: + if id is None: + return + + for guild in self.guilds: + sound = guild._resolve_soundboard_sound(id) + if sound is not None: + return sound + class AutoShardedConnectionState(ConnectionState[ClientT]): def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index cd949709a479..2c37542fddc7 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -88,6 +88,9 @@ 111, 112, 121, + 130, + 131, + 132, 140, 141, 142, @@ -112,6 +115,7 @@ class _AuditLogChange_Str(TypedDict): 'permissions', 'tags', 'unicode_emoji', + 'emoji_name', ] new_value: str old_value: str @@ -136,6 +140,8 @@ class _AuditLogChange_Snowflake(TypedDict): 'channel_id', 'inviter_id', 'guild_id', + 'user_id', + 'sound_id', ] new_value: Snowflake old_value: Snowflake @@ -183,6 +189,12 @@ class _AuditLogChange_Int(TypedDict): old_value: int +class _AuditLogChange_Float(TypedDict): + key: Literal['volume'] + new_value: float + old_value: float + + class _AuditLogChange_ListRole(TypedDict): key: Literal['$add', '$remove'] new_value: List[Role] @@ -290,6 +302,7 @@ class _AuditLogChange_TriggerMetadata(TypedDict): _AuditLogChange_AssetHash, _AuditLogChange_Snowflake, _AuditLogChange_Int, + _AuditLogChange_Float, _AuditLogChange_Bool, _AuditLogChange_ListRole, _AuditLogChange_MFALevel, diff --git a/discord/types/channel.py b/discord/types/channel.py index d5d82b5c6461..4b593e55426a 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -28,6 +28,7 @@ from .user import PartialUser from .snowflake import Snowflake from .threads import ThreadMetadata, ThreadMember, ThreadArchiveDuration, ThreadType +from .emoji import PartialEmoji OverwriteType = Literal[0, 1] @@ -89,6 +90,20 @@ class VoiceChannel(_BaseTextChannel): video_quality_mode: NotRequired[VideoQualityMode] +VoiceChannelEffectAnimationType = Literal[0, 1] + + +class VoiceChannelEffect(TypedDict): + guild_id: Snowflake + channel_id: Snowflake + user_id: Snowflake + emoji: NotRequired[Optional[PartialEmoji]] + animation_type: NotRequired[VoiceChannelEffectAnimationType] + animation_id: NotRequired[int] + sound_id: NotRequired[Union[int, str]] + sound_volume: NotRequired[float] + + class CategoryChannel(_BaseGuildChannel): type: Literal[4] diff --git a/discord/types/emoji.py b/discord/types/emoji.py index d54690c14417..85e7097576ca 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.py @@ -23,6 +23,7 @@ """ from typing import Optional, TypedDict +from typing_extensions import NotRequired from .snowflake import Snowflake, SnowflakeList from .user import User @@ -30,6 +31,7 @@ class PartialEmoji(TypedDict): id: Optional[Snowflake] name: Optional[str] + animated: NotRequired[bool] class Emoji(PartialEmoji, total=False): diff --git a/discord/types/gateway.py b/discord/types/gateway.py index ff43a5f25e70..974ceb20460b 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -31,7 +31,7 @@ from .voice import GuildVoiceState from .integration import BaseIntegration, IntegrationApplication from .role import Role -from .channel import ChannelType, StageInstance +from .channel import ChannelType, StageInstance, VoiceChannelEffect from .interactions import Interaction from .invite import InviteTargetType from .emoji import Emoji, PartialEmoji @@ -45,6 +45,7 @@ from .threads import Thread, ThreadMember from .scheduled_event import GuildScheduledEvent from .audit_log import AuditLogEntry +from .soundboard import SoundboardSound class SessionStartLimit(TypedDict): @@ -319,6 +320,15 @@ class _GuildScheduledEventUsersEvent(TypedDict): GuildScheduledEventUserAdd = GuildScheduledEventUserRemove = _GuildScheduledEventUsersEvent VoiceStateUpdateEvent = GuildVoiceState +VoiceChannelEffectSendEvent = VoiceChannelEffect + +GuildSoundBoardSoundCreateEvent = GuildSoundBoardSoundUpdateEvent = SoundboardSound +GuildSoundBoardSoundsUpdateEvent = List[SoundboardSound] + + +class GuildSoundBoardSoundDeleteEvent(TypedDict): + sound_id: Snowflake + guild_id: Snowflake class VoiceServerUpdateEvent(TypedDict): diff --git a/discord/types/guild.py b/discord/types/guild.py index ba43fbf96c14..e0a1f3e54438 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -37,6 +37,7 @@ from .emoji import Emoji from .user import User from .threads import Thread +from .soundboard import SoundboardSound class Ban(TypedDict): @@ -90,6 +91,8 @@ class IncidentData(TypedDict): 'VIP_REGIONS', 'WELCOME_SCREEN_ENABLED', 'RAID_ALERTS_DISABLED', + 'SOUNDBOARD', + 'MORE_SOUNDBOARD', ] @@ -154,6 +157,7 @@ class Guild(_BaseGuildPreview): max_members: NotRequired[int] premium_subscription_count: NotRequired[int] max_video_channel_users: NotRequired[int] + soundboard_sounds: NotRequired[List[SoundboardSound]] class InviteGuild(Guild, total=False): diff --git a/discord/types/soundboard.py b/discord/types/soundboard.py new file mode 100644 index 000000000000..4910df8082f5 --- /dev/null +++ b/discord/types/soundboard.py @@ -0,0 +1,49 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from typing import TypedDict, Optional, Union +from typing_extensions import NotRequired + +from .snowflake import Snowflake +from .user import User + + +class BaseSoundboardSound(TypedDict): + sound_id: Union[Snowflake, str] # basic string number when it's a default sound + volume: float + + +class SoundboardSound(BaseSoundboardSound): + name: str + emoji_name: Optional[str] + emoji_id: Optional[Snowflake] + user_id: NotRequired[Snowflake] + available: bool + guild_id: NotRequired[Snowflake] + user: NotRequired[User] + + +class SoundboardDefaultSound(BaseSoundboardSound): + name: str + emoji_name: str diff --git a/discord/utils.py b/discord/utils.py index 89cc8bdebfce..ee4097c467a6 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -623,9 +623,19 @@ def _get_mime_type_for_image(data: bytes): raise ValueError('Unsupported image type given') -def _bytes_to_base64_data(data: bytes) -> str: +def _get_mime_type_for_audio(data: bytes): + if data.startswith(b'\x49\x44\x33') or data.startswith(b'\xff\xfb'): + return 'audio/mpeg' + else: + raise ValueError('Unsupported audio type given') + + +def _bytes_to_base64_data(data: bytes, *, audio: bool = False) -> str: fmt = 'data:{mime};base64,{data}' - mime = _get_mime_type_for_image(data) + if audio: + mime = _get_mime_type_for_audio(data) + else: + mime = _get_mime_type_for_image(data) b64 = b64encode(data).decode('ascii') return fmt.format(mime=mime, data=b64) diff --git a/docs/api.rst b/docs/api.rst index e415ea8ceb07..c4feaa246501 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1298,6 +1298,35 @@ Scheduled Events :type user: :class:`User` +Soundboard +~~~~~~~~~~~ + +.. function:: on_soundboard_sound_create(sound) + on_soundboard_sound_delete(sound) + + Called when a :class:`SoundboardSound` is created or deleted. + + .. versionadded:: 2.5 + + :param sound: The soundboard sound that was created or deleted. + :type sound: :class:`SoundboardSound` + +.. function:: on_soundboard_sound_update(before, after) + + Called when a :class:`SoundboardSound` is updated. + + The following examples illustrate when this event is called: + + - The name is changed. + - The emoji is changed. + - The volume is changed. + + .. versionadded:: 2.5 + + :param sound: The soundboard sound that was updated. + :type sound: :class:`SoundboardSound` + + Stages ~~~~~~~ @@ -1483,6 +1512,17 @@ Voice :param after: The voice state after the changes. :type after: :class:`VoiceState` +.. function:: on_voice_channel_effect(effect) + + Called when a :class:`Member` sends a :class:`VoiceChannelEffect` in a voice channel the bot is in. + + This requires :attr:`Intents.voice_states` to be enabled. + + .. versionadded:: 2.5 + + :param effect: The effect that is sent. + :type effect: :class:`VoiceChannelEffect` + .. _discord-api-utils: Utility Functions @@ -2945,6 +2985,42 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: soundboard_sound_create + + A soundboard sound was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + + .. attribute:: soundboard_sound_update + + A soundboard sound was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + + .. attribute:: soundboard_sound_delete + + A soundboard sound was deleted. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.emoji` + - :attr:`~AuditLogDiff.volume` + + .. versionadded:: 2.5 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -3663,6 +3739,21 @@ of :class:`enum.Enum`. A burst reaction, also known as a "super reaction". +.. class:: VoiceChannelEffectAnimationType + + Represents the animation type of a voice channel effect. + + .. versionadded:: 2.5 + + .. attribute:: premium + + A fun animation, sent by a Nitro subscriber. + + .. attribute:: basic + + The standard animation. + + .. _discord-api-audit-logs: Audit Log Data @@ -4128,11 +4219,12 @@ AuditLogDiff .. attribute:: emoji - The name of the emoji that represents a sticker being changed. + The emoji which represents one of the following: - See also :attr:`GuildSticker.emoji`. + * :attr:`GuildSticker.emoji` + * :attr:`SoundboardSound.emoji` - :type: :class:`str` + :type: Union[:class:`str`, :class:`PartialEmoji`] .. attribute:: unicode_emoji @@ -4153,9 +4245,10 @@ AuditLogDiff .. attribute:: available - The availability of a sticker being changed. + The availability of one of the following being changed: - See also :attr:`GuildSticker.available` + * :attr:`GuildSticker.available` + * :attr:`SoundboardSound.available` :type: :class:`bool` @@ -4378,6 +4471,22 @@ AuditLogDiff :type: Optional[:class:`PartialEmoji`] + .. attribute:: user + + The user that represents the uploader of a soundboard sound. + + See also :attr:`SoundboardSound.user` + + :type: Union[:class:`Member`, :class:`User`] + + .. attribute:: volume + + The volume of a soundboard sound. + + See also :attr:`SoundboardSound.volume` + + :type: :class:`float` + .. this is currently missing the following keys: reason and application_id I'm not sure how to port these @@ -4799,6 +4908,35 @@ VoiceChannel :members: :inherited-members: +.. attributetable:: VoiceChannelEffect + +.. autoclass:: VoiceChannelEffect() + :members: + :inherited-members: + +.. class:: VoiceChannelEffectAnimation + + A namedtuple which represents a voice channel effect animation. + + .. versionadded:: 2.5 + + .. attribute:: id + + The ID of the animation. + + :type: :class:`int` + .. attribute:: type + + The type of the animation. + + :type: :class:`VoiceChannelEffectAnimationType` + +.. attributetable:: VoiceChannelSoundEffect + +.. autoclass:: VoiceChannelSoundEffect() + :members: + :inherited-members: + StageChannel ~~~~~~~~~~~~~ @@ -4965,6 +5103,30 @@ GuildSticker .. autoclass:: GuildSticker() :members: +BaseSoundboardSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: BaseSoundboardSound + +.. autoclass:: BaseSoundboardSound() + :members: + +SoundboardDefaultSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SoundboardDefaultSound + +.. autoclass:: SoundboardDefaultSound() + :members: + +SoundboardSound +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SoundboardSound + +.. autoclass:: SoundboardSound() + :members: + ShardInfo ~~~~~~~~~~~ From 0298f81a5cd39f455c14b7f5ffb81c6f28f74e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jun-Ah=20=EC=A4=80=EC=95=84?= Date: Thu, 10 Oct 2024 06:15:37 +0900 Subject: [PATCH 114/354] Add more tests for colour, embeds, files, buttons and modals --- tests/test_colour.py | 66 ++++++++++ tests/test_embed.py | 269 +++++++++++++++++++++++++++++++++++++++ tests/test_files.py | 56 ++++++++ tests/test_ui_buttons.py | 167 ++++++++++++++++++++++++ tests/test_ui_modals.py | 102 +++++++++++++++ 5 files changed, 660 insertions(+) create mode 100644 tests/test_embed.py create mode 100644 tests/test_ui_buttons.py create mode 100644 tests/test_ui_modals.py diff --git a/tests/test_colour.py b/tests/test_colour.py index bf0e597133a6..b79f153f06e8 100644 --- a/tests/test_colour.py +++ b/tests/test_colour.py @@ -44,6 +44,7 @@ ('rgb(20%, 24%, 56%)', 0x333D8F), ('rgb(20%, 23.9%, 56.1%)', 0x333D8F), ('rgb(51, 61, 143)', 0x333D8F), + ('0x#333D8F', 0x333D8F), ], ) def test_from_str(value, expected): @@ -53,6 +54,7 @@ def test_from_str(value, expected): @pytest.mark.parametrize( ('value'), [ + None, 'not valid', '0xYEAH', '#YEAH', @@ -62,8 +64,72 @@ def test_from_str(value, expected): 'rgb(30, -1, 60)', 'invalid(a, b, c)', 'rgb(', + '#1000000', + '#FFFFFFF', + "rgb(101%, 50%, 50%)", + "rgb(50%, -10%, 50%)", + "rgb(50%, 50%, 150%)", + "rgb(256, 100, 100)", ], ) def test_from_str_failures(value): with pytest.raises(ValueError): discord.Colour.from_str(value) + + +@pytest.mark.parametrize( + ('value', 'expected'), + [ + (discord.Colour.default(), 0x000000), + (discord.Colour.teal(), 0x1ABC9C), + (discord.Colour.dark_teal(), 0x11806A), + (discord.Colour.brand_green(), 0x57F287), + (discord.Colour.green(), 0x2ECC71), + (discord.Colour.dark_green(), 0x1F8B4C), + (discord.Colour.blue(), 0x3498DB), + (discord.Colour.dark_blue(), 0x206694), + (discord.Colour.purple(), 0x9B59B6), + (discord.Colour.dark_purple(), 0x71368A), + (discord.Colour.magenta(), 0xE91E63), + (discord.Colour.dark_magenta(), 0xAD1457), + (discord.Colour.gold(), 0xF1C40F), + (discord.Colour.dark_gold(), 0xC27C0E), + (discord.Colour.orange(), 0xE67E22), + (discord.Colour.dark_orange(), 0xA84300), + (discord.Colour.brand_red(), 0xED4245), + (discord.Colour.red(), 0xE74C3C), + (discord.Colour.dark_red(), 0x992D22), + (discord.Colour.lighter_grey(), 0x95A5A6), + (discord.Colour.dark_grey(), 0x607D8B), + (discord.Colour.light_grey(), 0x979C9F), + (discord.Colour.darker_grey(), 0x546E7A), + (discord.Colour.og_blurple(), 0x7289DA), + (discord.Colour.blurple(), 0x5865F2), + (discord.Colour.greyple(), 0x99AAB5), + (discord.Colour.dark_theme(), 0x313338), + (discord.Colour.fuchsia(), 0xEB459E), + (discord.Colour.yellow(), 0xFEE75C), + (discord.Colour.dark_embed(), 0x2B2D31), + (discord.Colour.light_embed(), 0xEEEFF1), + (discord.Colour.pink(), 0xEB459F), + ], +) +def test_static_colours(value, expected): + assert value.value == expected + + + + +@pytest.mark.parametrize( + ('value', 'property', 'expected'), + [ + (discord.Colour(0x000000), 'r', 0), + (discord.Colour(0xFFFFFF), 'g', 255), + (discord.Colour(0xABCDEF), 'b', 239), + (discord.Colour(0x44243B), 'r', 68), + (discord.Colour(0x333D8F), 'g', 61), + (discord.Colour(0xDBFF00), 'b', 0), + ], +) +def test_colour_properties(value, property, expected): + assert getattr(value, property) == expected diff --git a/tests/test_embed.py b/tests/test_embed.py new file mode 100644 index 000000000000..3efedd6a57be --- /dev/null +++ b/tests/test_embed.py @@ -0,0 +1,269 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import datetime + +import discord +import pytest + + +@pytest.mark.parametrize( + ('title', 'description', 'colour', 'url'), + [ + ('title', 'description', 0xABCDEF, 'https://example.com'), + ('title', 'description', 0xFF1294, None), + ('title', 'description', discord.Colour(0x333D8F), 'https://example.com'), + ('title', 'description', discord.Colour(0x44243B), None), + ], +) +def test_embed_initialization(title, description, colour, url): + embed = discord.Embed(title=title, description=description, colour=colour, url=url) + assert embed.title == title + assert embed.description == description + assert embed.colour == colour or embed.colour == discord.Colour(colour) + assert embed.url == url + + +@pytest.mark.parametrize( + ('text', 'icon_url'), + [ + ('Hello discord.py', 'https://example.com'), + ('text', None), + (None, 'https://example.com'), + (None, None), + ], +) +def test_embed_set_footer(text, icon_url): + embed = discord.Embed() + embed.set_footer(text=text, icon_url=icon_url) + assert embed.footer.text == text + assert embed.footer.icon_url == icon_url + + +def test_embed_remove_footer(): + embed = discord.Embed() + embed.set_footer(text='Hello discord.py', icon_url='https://example.com') + embed.remove_footer() + assert embed.footer.text is None + assert embed.footer.icon_url is None + + +@pytest.mark.parametrize( + ('name', 'url', 'icon_url'), + [ + ('Rapptz', 'http://example.com', 'http://example.com/icon.png'), + ('NCPlayz', None, 'http://example.com/icon.png'), + ('Jackenmen', 'http://example.com', None), + ], +) +def test_embed_set_author(name, url, icon_url): + embed = discord.Embed() + embed.set_author(name=name, url=url, icon_url=icon_url) + assert embed.author.name == name + assert embed.author.url == url + assert embed.author.icon_url == icon_url + + +def test_embed_remove_author(): + embed = discord.Embed() + embed.set_author(name='Rapptz', url='http://example.com', icon_url='http://example.com/icon.png') + embed.remove_author() + assert embed.author.name is None + assert embed.author.url is None + assert embed.author.icon_url is None + + +@pytest.mark.parametrize( + ('thumbnail'), + [ + ('http://example.com'), + (None), + ], +) +def test_embed_set_thumbnail(thumbnail): + embed = discord.Embed() + embed.set_thumbnail(url=thumbnail) + assert embed.thumbnail.url == thumbnail + + +@pytest.mark.parametrize( + ('image'), + [ + ('http://example.com'), + (None), + ], +) +def test_embed_set_image(image): + embed = discord.Embed() + embed.set_image(url=image) + assert embed.image.url == image + + +@pytest.mark.parametrize( + ('name', 'value', 'inline'), + [ + ('music', 'music value', True), + ('sport', 'sport value', False), + ], +) +def test_embed_add_field(name, value, inline): + embed = discord.Embed() + embed.add_field(name=name, value=value, inline=inline) + assert len(embed.fields) == 1 + assert embed.fields[0].name == name + assert embed.fields[0].value == value + assert embed.fields[0].inline == inline + + +def test_embed_insert_field(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.insert_field_at(0, name='name 2', value='value 2', inline=False) + assert embed.fields[0].name == 'name 2' + assert embed.fields[0].value == 'value 2' + assert embed.fields[0].inline is False + + +def test_embed_set_field_at(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.set_field_at(0, name='name 2', value='value 2', inline=False) + assert embed.fields[0].name == 'name 2' + assert embed.fields[0].value == 'value 2' + assert embed.fields[0].inline is False + + +def test_embed_set_field_at_failure(): + embed = discord.Embed() + with pytest.raises(IndexError): + embed.set_field_at(0, name='name', value='value', inline=True) + + +def test_embed_clear_fields(): + embed = discord.Embed() + embed.add_field(name="field 1", value="value 1", inline=False) + embed.add_field(name="field 2", value="value 2", inline=False) + embed.add_field(name="field 3", value="value 3", inline=False) + embed.clear_fields() + assert len(embed.fields) == 0 + + +def test_embed_remove_field(): + embed = discord.Embed() + embed.add_field(name='name', value='value', inline=True) + embed.remove_field(0) + assert len(embed.fields) == 0 + + +@pytest.mark.parametrize( + ('title', 'description', 'url'), + [ + ('title 1', 'description 1', 'https://example.com'), + ('title 2', 'description 2', None), + ], +) +def test_embed_copy(title, description, url): + embed = discord.Embed(title=title, description=description, url=url) + embed_copy = embed.copy() + + assert embed == embed_copy + assert embed.title == embed_copy.title + assert embed.description == embed_copy.description + assert embed.url == embed_copy.url + + +@pytest.mark.parametrize( + ('title', 'description'), + [ + ('title 1', 'description 1'), + ('title 2', 'description 2'), + ], +) +def test_embed_len(title, description): + embed = discord.Embed(title=title, description=description) + assert len(embed) == len(title) + len(description) + + +@pytest.mark.parametrize( + ('title', 'description', 'fields', 'footer', 'author'), + [ + ( + 'title 1', + 'description 1', + [('field name 1', 'field value 1'), ('field name 2', 'field value 2')], + 'footer 1', + 'author 1', + ), + ('title 2', 'description 2', [('field name 3', 'field value 3')], 'footer 2', 'author 2'), + ], +) +def test_embed_len_with_options(title, description, fields, footer, author): + embed = discord.Embed(title=title, description=description) + for name, value in fields: + embed.add_field(name=name, value=value) + embed.set_footer(text=footer) + embed.set_author(name=author) + assert len(embed) == len(title) + len(description) + len("".join([name + value for name, value in fields])) + len( + footer + ) + len(author) + + +def test_embed_to_dict(): + timestamp = datetime.datetime.now(datetime.timezone.utc) + embed = discord.Embed(title="Test Title", description="Test Description", timestamp=timestamp) + data = embed.to_dict() + assert data['title'] == "Test Title" + assert data['description'] == "Test Description" + assert data['timestamp'] == timestamp.isoformat() + + +def test_embed_from_dict(): + data = { + 'title': 'Test Title', + 'description': 'Test Description', + 'url': 'http://example.com', + 'color': 0x00FF00, + 'timestamp': '2024-07-03T12:34:56+00:00', + } + embed = discord.Embed.from_dict(data) + assert embed.title == 'Test Title' + assert embed.description == 'Test Description' + assert embed.url == 'http://example.com' + assert embed.colour is not None and embed.colour.value == 0x00FF00 + assert embed.timestamp is not None and embed.timestamp.isoformat() == '2024-07-03T12:34:56+00:00' + + +@pytest.mark.parametrize( + ('value'), + [ + -0.5, + '#FFFFFF', + ], +) +def test_embed_colour_setter_failure(value): + embed = discord.Embed() + with pytest.raises(TypeError): + embed.colour = value diff --git a/tests/test_files.py b/tests/test_files.py index 6096c3a3891b..72ff3b7b37cf 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -27,6 +27,7 @@ from io import BytesIO import discord +import pytest FILE = BytesIO() @@ -127,3 +128,58 @@ def test_file_not_spoiler_with_overriding_name_double_spoiler(): f.filename = 'SPOILER_SPOILER_.gitignore' assert f.filename == 'SPOILER_.gitignore' assert f.spoiler == True + + +def test_file_reset(): + f = discord.File('.gitignore') + + f.reset(seek=True) + assert f.fp.tell() == 0 + + f.reset(seek=False) + assert f.fp.tell() == 0 + + +def test_io_reset(): + f = discord.File(FILE) + + f.reset(seek=True) + assert f.fp.tell() == 0 + + f.reset(seek=False) + assert f.fp.tell() == 0 + + +def test_io_failure(): + class NonSeekableReadable(BytesIO): + def seekable(self): + return False + + def readable(self): + return False + + f = NonSeekableReadable() + + with pytest.raises(ValueError) as excinfo: + discord.File(f) + + assert str(excinfo.value) == f"File buffer {f!r} must be seekable and readable" + + +def test_io_to_dict(): + buffer = BytesIO(b"test content") + file = discord.File(buffer, filename="test.txt", description="test description") + + data = file.to_dict(0) + assert data["id"] == 0 + assert data["filename"] == "test.txt" + assert data["description"] == "test description" + + +def test_file_to_dict(): + f = discord.File('.gitignore', description="test description") + + data = f.to_dict(0) + assert data["id"] == 0 + assert data["filename"] == ".gitignore" + assert data["description"] == "test description" diff --git a/tests/test_ui_buttons.py b/tests/test_ui_buttons.py new file mode 100644 index 000000000000..55c0c7cd8269 --- /dev/null +++ b/tests/test_ui_buttons.py @@ -0,0 +1,167 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +def test_button_init(): + button = discord.ui.Button( + label="Click me!", + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.secondary + assert button.disabled == False + assert button.url == None + assert button.emoji == None + assert button.sku_id == None + + +def test_button_with_sku_id(): + button = discord.ui.Button( + label="Click me!", + sku_id=1234567890, + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.premium + assert button.sku_id == 1234567890 + + +def test_button_with_url(): + button = discord.ui.Button( + label="Click me!", + url="https://example.com", + ) + assert button.label == "Click me!" + assert button.style == discord.ButtonStyle.link + assert button.url == "https://example.com" + + +def test_mix_both_custom_id_and_url(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + url="https://example.com", + custom_id="test", + ) + + +def test_mix_both_custom_id_and_sku_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + sku_id=1234567890, + custom_id="test", + ) + + +def test_mix_both_url_and_sku_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + url="https://example.com", + sku_id=1234567890, + ) + + +def test_invalid_url(): + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.url = 1234567890 # type: ignore + + +def test_invalid_custom_id(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + custom_id=1234567890, # type: ignore + ) + + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.custom_id = 1234567890 # type: ignore + + +def test_button_with_partial_emoji(): + button = discord.ui.Button( + label="Click me!", + emoji="👍", + ) + assert button.label == "Click me!" + assert button.emoji is not None and button.emoji.name == "👍" + + +def test_button_with_str_emoji(): + emoji = discord.PartialEmoji(name="👍") + button = discord.ui.Button( + label="Click me!", + emoji=emoji, + ) + assert button.label == "Click me!" + assert button.emoji == emoji + + +def test_button_with_invalid_emoji(): + with pytest.raises(TypeError): + discord.ui.Button( + label="Click me!", + emoji=-0.53, # type: ignore + ) + + button = discord.ui.Button( + label="Click me!", + ) + with pytest.raises(TypeError): + button.emoji = -0.53 # type: ignore + + +def test_button_setter(): + button = discord.ui.Button() + + button.label = "Click me!" + assert button.label == "Click me!" + + button.style = discord.ButtonStyle.primary + assert button.style == discord.ButtonStyle.primary + + button.disabled = True + assert button.disabled == True + + button.url = "https://example.com" + assert button.url == "https://example.com" + + button.emoji = "👍" + assert button.emoji is not None and button.emoji.name == "👍" # type: ignore + + button.custom_id = "test" + assert button.custom_id == "test" + + button.sku_id = 1234567890 + assert button.sku_id == 1234567890 diff --git a/tests/test_ui_modals.py b/tests/test_ui_modals.py new file mode 100644 index 000000000000..dd1ac7169187 --- /dev/null +++ b/tests/test_ui_modals.py @@ -0,0 +1,102 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +@pytest.mark.asyncio +async def test_modal_init(): + modal = discord.ui.Modal( + title="Temp Title", + ) + assert modal.title == "Temp Title" + assert modal.timeout == None + + +@pytest.mark.asyncio +async def test_no_title(): + with pytest.raises(ValueError) as excinfo: + discord.ui.Modal() + + assert str(excinfo.value) == "Modal must have a title" + + +@pytest.mark.asyncio +async def test_to_dict(): + modal = discord.ui.Modal( + title="Temp Title", + ) + data = modal.to_dict() + assert data["custom_id"] is not None + assert data["title"] == "Temp Title" + assert data["components"] == [] + + +@pytest.mark.asyncio +async def test_add_item(): + modal = discord.ui.Modal( + title="Temp Title", + ) + item = discord.ui.TextInput(label="Test") + modal.add_item(item) + + assert modal.children == [item] + + +@pytest.mark.asyncio +async def test_add_item_invalid(): + modal = discord.ui.Modal( + title="Temp Title", + ) + with pytest.raises(TypeError): + modal.add_item("Not an item") # type: ignore + + +@pytest.mark.asyncio +async def test_maximum_items(): + modal = discord.ui.Modal( + title="Temp Title", + ) + max_item_limit = 5 + + for i in range(max_item_limit): + modal.add_item(discord.ui.TextInput(label=f"Test {i}")) + + with pytest.raises(ValueError): + modal.add_item(discord.ui.TextInput(label="Test")) + + +@pytest.mark.asyncio +async def test_modal_setters(): + modal = discord.ui.Modal( + title="Temp Title", + ) + modal.title = "New Title" + assert modal.title == "New Title" + + modal.timeout = 120 + assert modal.timeout == 120 From 053f29c96c01c96f209a2e78efdbc767f2251958 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:27:02 +0200 Subject: [PATCH 115/354] Update all channel clone implementations --- discord/channel.py | 55 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 37f0ab6fa718..9789b7b3fecc 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -527,7 +527,15 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona @utils.copy_doc(discord.abc.GuildChannel.clone) async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> TextChannel: return await self._clone_impl( - {'topic': self.topic, 'nsfw': self.nsfw, 'rate_limit_per_user': self.slowmode_delay}, name=name, reason=reason + { + 'topic': self.topic, + 'rate_limit_per_user': self.slowmode_delay, + 'nsfw': self.nsfw, + 'default_auto_archive_duration': self.default_auto_archive_duration, + 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, + }, + name=name, + reason=reason, ) async def delete_messages(self, messages: Iterable[Snowflake], *, reason: Optional[str] = None) -> None: @@ -1379,6 +1387,24 @@ async def create_webhook(self, *, name: str, avatar: Optional[bytes] = None, rea data = await self._state.http.create_webhook(self.id, name=str(name), avatar=avatar, reason=reason) return Webhook.from_state(data, state=self._state) + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> Self: + base = { + 'bitrate': self.bitrate, + 'user_limit': self.user_limit, + 'rate_limit_per_user': self.slowmode_delay, + 'nsfw': self.nsfw, + 'video_quality_mode': self.video_quality_mode.value, + } + if self.rtc_region: + base['rtc_region'] = self.rtc_region + + return await self._clone_impl( + base, + name=name, + reason=reason, + ) + class VoiceChannel(VocalGuildChannel): """Represents a Discord guild voice channel. @@ -1473,10 +1499,6 @@ def type(self) -> Literal[ChannelType.voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> VoiceChannel: - return await self._clone_impl({'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, reason=reason) - @overload async def edit(self) -> None: ... @@ -1747,10 +1769,6 @@ def type(self) -> Literal[ChannelType.stage_voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.stage_voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> StageChannel: - return await self._clone_impl({}, name=name, reason=reason) - @property def instance(self) -> Optional[StageInstance]: """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. @@ -2546,8 +2564,25 @@ def is_media(self) -> bool: @utils.copy_doc(discord.abc.GuildChannel.clone) async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> ForumChannel: + base = { + 'topic': self.topic, + 'rate_limit_per_user': self.slowmode_delay, + 'nsfw': self.nsfw, + 'default_auto_archive_duration': self.default_auto_archive_duration, + 'available_tags': [tag.to_dict() for tag in self.available_tags], + 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, + } + if self.default_sort_order: + base['default_sort_order'] = self.default_sort_order.value + if self.default_reaction_emoji: + base['default_reaction_emoji'] = self.default_reaction_emoji._to_forum_tag_payload() + if not self.is_media() and self.default_layout: + base['default_forum_layout'] = self.default_layout.value + return await self._clone_impl( - {'topic': self.topic, 'nsfw': self.nsfw, 'rate_limit_per_user': self.slowmode_delay}, name=name, reason=reason + base, + name=name, + reason=reason, ) @overload From 3e168a93bf94599e64a827e292779df586ba59f6 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 9 Oct 2024 17:27:55 -0400 Subject: [PATCH 116/354] Improve typing of app command transformers This allows subclasses of transformers to specify a specialization for interaction without violating covariance of parameter types --- discord/app_commands/transformers.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index d012c52b98af..e7b001727343 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -34,6 +34,7 @@ ClassVar, Coroutine, Dict, + Generic, List, Literal, Optional, @@ -56,6 +57,7 @@ from ..role import Role from ..member import Member from ..message import Attachment +from .._types import ClientT __all__ = ( 'Transformer', @@ -191,7 +193,7 @@ def display_name(self) -> str: return self.name if self._rename is MISSING else str(self._rename) -class Transformer: +class Transformer(Generic[ClientT]): """The base class that allows a type annotation in an application command parameter to map into a :class:`~discord.AppCommandOptionType` and transform the raw value into one from this type. @@ -304,7 +306,7 @@ def _error_display_name(self) -> str: else: return name - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: """|maybecoro| Transforms the converted option value into another value. @@ -324,7 +326,7 @@ async def transform(self, interaction: Interaction, value: Any, /) -> Any: raise NotImplementedError('Derived classes need to implement this.') async def autocomplete( - self, interaction: Interaction, value: Union[int, float, str], / + self, interaction: Interaction[ClientT], value: Union[int, float, str], / ) -> List[Choice[Union[int, float, str]]]: """|coro| @@ -352,7 +354,7 @@ async def autocomplete( raise NotImplementedError('Derived classes can implement this.') -class IdentityTransformer(Transformer): +class IdentityTransformer(Transformer[ClientT]): def __init__(self, type: AppCommandOptionType) -> None: self._type = type @@ -360,7 +362,7 @@ def __init__(self, type: AppCommandOptionType) -> None: def type(self) -> AppCommandOptionType: return self._type - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: return value @@ -489,7 +491,7 @@ async def transform(self, interaction: Interaction, value: Any, /) -> Any: return self._enum[value] -class InlineTransformer(Transformer): +class InlineTransformer(Transformer[ClientT]): def __init__(self, annotation: Any) -> None: super().__init__() self.annotation: Any = annotation @@ -502,7 +504,7 @@ def _error_display_name(self) -> str: def type(self) -> AppCommandOptionType: return AppCommandOptionType.string - async def transform(self, interaction: Interaction, value: Any, /) -> Any: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Any: return await self.annotation.transform(interaction, value) @@ -611,18 +613,18 @@ def __class_getitem__(cls, obj) -> RangeTransformer: return transformer -class MemberTransformer(Transformer): +class MemberTransformer(Transformer[ClientT]): @property def type(self) -> AppCommandOptionType: return AppCommandOptionType.user - async def transform(self, interaction: Interaction, value: Any, /) -> Member: + async def transform(self, interaction: Interaction[ClientT], value: Any, /) -> Member: if not isinstance(value, Member): raise TransformerError(value, self.type, self) return value -class BaseChannelTransformer(Transformer): +class BaseChannelTransformer(Transformer[ClientT]): def __init__(self, *channel_types: Type[Any]) -> None: super().__init__() if len(channel_types) == 1: @@ -654,22 +656,22 @@ def type(self) -> AppCommandOptionType: def channel_types(self) -> List[ChannelType]: return self._channel_types - async def transform(self, interaction: Interaction, value: Any, /): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): resolved = value.resolve() if resolved is None or not isinstance(resolved, self._types): raise TransformerError(value, AppCommandOptionType.channel, self) return resolved -class RawChannelTransformer(BaseChannelTransformer): - async def transform(self, interaction: Interaction, value: Any, /): +class RawChannelTransformer(BaseChannelTransformer[ClientT]): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): if not isinstance(value, self._types): raise TransformerError(value, AppCommandOptionType.channel, self) return value -class UnionChannelTransformer(BaseChannelTransformer): - async def transform(self, interaction: Interaction, value: Any, /): +class UnionChannelTransformer(BaseChannelTransformer[ClientT]): + async def transform(self, interaction: Interaction[ClientT], value: Any, /): if isinstance(value, self._types): return value From d10e70e04cced1c010dbbaaff193b9c6cd1674aa Mon Sep 17 00:00:00 2001 From: lmaotrigine <57328245+lmaotrigine@users.noreply.github.com> Date: Thu, 10 Oct 2024 02:59:15 +0530 Subject: [PATCH 117/354] [docs] Fix spelling --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 30c3a1c6f9c6..6ee4a003d33f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -251,7 +251,7 @@ class Client: .. versionadded:: 2.0 connector: Optional[:class:`aiohttp.BaseConnector`] - The aiohhtp connector to use for this client. This can be used to control underlying aiohttp + The aiohttp connector to use for this client. This can be used to control underlying aiohttp behavior, such as setting a dns resolver or sslcontext. .. versionadded:: 2.5 From 91f300a28ad42dc03bfae3399f9ac51ece44802a Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Wed, 9 Oct 2024 23:30:03 +0200 Subject: [PATCH 118/354] Add zstd gateway compression to speed profile --- discord/gateway.py | 23 +++++++++--------- discord/http.py | 19 ++------------- discord/utils.py | 58 +++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 1 + 4 files changed, 69 insertions(+), 32 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index e6fb7d8bfac5..13a213ce3ee9 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -32,7 +33,6 @@ import time import threading import traceback -import zlib from typing import Any, Callable, Coroutine, Deque, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar, Tuple @@ -325,8 +325,7 @@ def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.Abs # ws related stuff self.session_id: Optional[str] = None self.sequence: Optional[int] = None - self._zlib: zlib._Decompress = zlib.decompressobj() - self._buffer: bytearray = bytearray() + self._decompressor: utils._DecompressionContext = utils._ActiveDecompressionContext() self._close_code: Optional[int] = None self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter() @@ -355,7 +354,7 @@ async def from_client( sequence: Optional[int] = None, resume: bool = False, encoding: str = 'json', - zlib: bool = True, + compress: bool = True, ) -> Self: """Creates a main websocket for Discord from a :class:`Client`. @@ -366,10 +365,12 @@ async def from_client( gateway = gateway or cls.DEFAULT_GATEWAY - if zlib: - url = gateway.with_query(v=INTERNAL_API_VERSION, encoding=encoding, compress='zlib-stream') - else: + if not compress: url = gateway.with_query(v=INTERNAL_API_VERSION, encoding=encoding) + else: + url = gateway.with_query( + v=INTERNAL_API_VERSION, encoding=encoding, compress=utils._ActiveDecompressionContext.COMPRESSION_TYPE + ) socket = await client.http.ws_connect(str(url)) ws = cls(socket, loop=client.loop) @@ -488,13 +489,11 @@ async def resume(self) -> None: async def received_message(self, msg: Any, /) -> None: if type(msg) is bytes: - self._buffer.extend(msg) + msg = self._decompressor.decompress(msg) - if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff': + # Received a partial gateway message + if msg is None: return - msg = self._zlib.decompress(self._buffer) - msg = msg.decode('utf-8') - self._buffer = bytearray() self.log_receive(msg) msg = utils._from_json(msg) diff --git a/discord/http.py b/discord/http.py index 24605c4fcd9c..3c1eacb61fb8 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2701,28 +2701,13 @@ def end_poll(self, channel_id: Snowflake, message_id: Snowflake) -> Response[mes # Misc - async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: - try: - data = await self.request(Route('GET', '/gateway')) - except HTTPException as exc: - raise GatewayNotFound() from exc - if zlib: - value = '{0}?encoding={1}&v={2}&compress=zlib-stream' - else: - value = '{0}?encoding={1}&v={2}' - return value.format(data['url'], encoding, INTERNAL_API_VERSION) - - async def get_bot_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> Tuple[int, str]: + async def get_bot_gateway(self) -> Tuple[int, str]: try: data = await self.request(Route('GET', '/gateway/bot')) except HTTPException as exc: raise GatewayNotFound() from exc - if zlib: - value = '{0}?encoding={1}&v={2}&compress=zlib-stream' - else: - value = '{0}?encoding={1}&v={2}' - return data['shards'], value.format(data['url'], encoding, INTERNAL_API_VERSION) + return data['shards'], data['url'] def get_user(self, user_id: Snowflake) -> Response[user.User]: return self.request(Route('GET', '/users/{user_id}', user_id=user_id)) diff --git a/discord/utils.py b/discord/utils.py index ee4097c467a6..cb7d662b62ec 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import array @@ -41,7 +42,6 @@ Iterator, List, Literal, - Mapping, NamedTuple, Optional, Protocol, @@ -71,6 +71,7 @@ import typing import warnings import logging +import zlib import yarl @@ -81,6 +82,12 @@ else: HAS_ORJSON = True +try: + import zstandard # type: ignore +except ImportError: + _HAS_ZSTD = False +else: + _HAS_ZSTD = True __all__ = ( 'oauth_url', @@ -148,8 +155,11 @@ def __get__(self, instance, owner): from .invite import Invite from .template import Template - class _RequestLike(Protocol): - headers: Mapping[str, Any] + class _DecompressionContext(Protocol): + COMPRESSION_TYPE: str + + def decompress(self, data: bytes, /) -> str | None: + ... P = ParamSpec('P') @@ -1416,3 +1426,45 @@ def _human_join(seq: Sequence[str], /, *, delimiter: str = ', ', final: str = 'o return f'{seq[0]} {final} {seq[1]}' return delimiter.join(seq[:-1]) + f' {final} {seq[-1]}' + + +if _HAS_ZSTD: + + class _ZstdDecompressionContext: + __slots__ = ('context',) + + COMPRESSION_TYPE: str = 'zstd-stream' + + def __init__(self) -> None: + decompressor = zstandard.ZstdDecompressor() + self.context = decompressor.decompressobj() + + def decompress(self, data: bytes, /) -> str | None: + # Each WS message is a complete gateway message + return self.context.decompress(data).decode('utf-8') + + _ActiveDecompressionContext: Type[_DecompressionContext] = _ZstdDecompressionContext +else: + + class _ZlibDecompressionContext: + __slots__ = ('context', 'buffer') + + COMPRESSION_TYPE: str = 'zlib-stream' + + def __init__(self) -> None: + self.buffer: bytearray = bytearray() + self.context = zlib.decompressobj() + + def decompress(self, data: bytes, /) -> str | None: + self.buffer.extend(data) + + # Check whether ending is Z_SYNC_FLUSH + if len(data) < 4 or data[-4:] != b'\x00\x00\xff\xff': + return + + msg = self.context.decompress(self.buffer) + self.buffer = bytearray() + + return msg.decode('utf-8') + + _ActiveDecompressionContext: Type[_DecompressionContext] = _ZlibDecompressionContext diff --git a/pyproject.toml b/pyproject.toml index 596e6ef0874d..4ec7bc007de7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ speed = [ "aiodns>=1.1; sys_platform != 'win32'", "Brotli", "cchardet==2.1.7; python_version < '3.10'", + "zstandard>=0.23.0" ] test = [ "coverage[toml]", From ec9fd57254860cc9eee0d1d543680be65217bbd9 Mon Sep 17 00:00:00 2001 From: Mysty Date: Thu, 10 Oct 2024 07:33:37 +1000 Subject: [PATCH 119/354] Add support for AEAD XChaCha20 Poly1305 encryption mode --- discord/types/voice.py | 7 ++++++- discord/voice_client.py | 25 ++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/discord/types/voice.py b/discord/types/voice.py index 8f4e2e03e9e5..7e856ecddef0 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -29,7 +29,12 @@ from .member import MemberWithUser -SupportedModes = Literal['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'] +SupportedModes = Literal[ + 'aead_xchacha20_poly1305_rtpsize', + 'xsalsa20_poly1305_lite', + 'xsalsa20_poly1305_suffix', + 'xsalsa20_poly1305', +] class _VoiceState(TypedDict): diff --git a/discord/voice_client.py b/discord/voice_client.py index 3e1c6a5ff967..795434e1e722 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -230,12 +230,13 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None: self.timestamp: int = 0 self._player: Optional[AudioPlayer] = None self.encoder: Encoder = MISSING - self._lite_nonce: int = 0 + self._incr_nonce: int = 0 self._connection: VoiceConnectionState = self.create_connection_state() warn_nacl: bool = not has_nacl supported_modes: Tuple[SupportedModes, ...] = ( + 'aead_xchacha20_poly1305_rtpsize', 'xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305', @@ -380,7 +381,21 @@ def _get_voice_packet(self, data): encrypt_packet = getattr(self, '_encrypt_' + self.mode) return encrypt_packet(header, data) + def _encrypt_aead_xchacha20_poly1305_rtpsize(self, header: bytes, data) -> bytes: + # Esentially the same as _lite + # Uses an incrementing 32-bit integer which is appended to the payload + # The only other difference is we require AEAD with Additional Authenticated Data (the header) + box = nacl.secret.Aead(bytes(self.secret_key)) + nonce = bytearray(24) + + nonce[:4] = struct.pack('>I', self._incr_nonce) + self.checked_add('_incr_nonce', 1, 4294967295) + + return header + box.encrypt(bytes(data), bytes(header), bytes(nonce)).ciphertext + nonce[:4] + def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) nonce[:12] = header @@ -388,17 +403,21 @@ def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes: return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext def _encrypt_xsalsa20_poly1305_suffix(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) return header + box.encrypt(bytes(data), nonce).ciphertext + nonce def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: + # Deprecated. Removal: 18th Nov 2024. See: + # https://discord.com/developers/docs/topics/voice-connections#transport-encryption-modes box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) - nonce[:4] = struct.pack('>I', self._lite_nonce) - self.checked_add('_lite_nonce', 1, 4294967295) + nonce[:4] = struct.pack('>I', self._incr_nonce) + self.checked_add('_incr_nonce', 1, 4294967295) return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] From 1ad45f585f271270d5215c6a20cc2c57279505e6 Mon Sep 17 00:00:00 2001 From: Gooraeng <101193491+Gooraeng@users.noreply.github.com> Date: Thu, 10 Oct 2024 06:46:07 +0900 Subject: [PATCH 120/354] Add missing error for Guild.fetch_automod_rule --- discord/guild.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 5fd60a853fa6..fc39179abeb2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4353,6 +4353,8 @@ async def fetch_automod_rule(self, automod_rule_id: int, /) -> AutoModRule: ------- Forbidden You do not have permission to view the automod rule. + NotFound + The automod rule does not exist within this guild. Returns -------- From 0ce75f3f53bdf7a8db034c793eb714fcec432831 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Thu, 10 Oct 2024 07:51:00 +1000 Subject: [PATCH 121/354] [commands] Fix issue with category cooldowns outside of guild channels --- discord/ext/commands/cooldowns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 9a73a2b4edab..cf328d9b3f5e 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -71,7 +71,7 @@ def get_key(self, msg: Union[Message, Context[Any]]) -> Any: elif self is BucketType.member: return ((msg.guild and msg.guild.id), msg.author.id) elif self is BucketType.category: - return (msg.channel.category or msg.channel).id # type: ignore + return (getattr(msg.channel, 'category', None) or msg.channel).id elif self is BucketType.role: # we return the channel id of a private-channel as there are only roles in guilds # and that yields the same result as for a guild with only the @everyone role From 58b6929aa54f7001cd465088081e903b53703b2b Mon Sep 17 00:00:00 2001 From: MCausc78 Date: Thu, 10 Oct 2024 01:04:14 +0300 Subject: [PATCH 122/354] Add SKU subscriptions support --- discord/__init__.py | 1 + discord/client.py | 25 ++++++ discord/enums.py | 7 ++ discord/http.py | 44 ++++++++++ discord/sku.py | 159 +++++++++++++++++++++++++++++++--- discord/state.py | 14 +++ discord/subscription.py | 103 ++++++++++++++++++++++ discord/types/gateway.py | 4 + discord/types/subscription.py | 42 +++++++++ docs/api.rst | 58 +++++++++++++ 10 files changed, 445 insertions(+), 12 deletions(-) create mode 100644 discord/subscription.py create mode 100644 discord/types/subscription.py diff --git a/discord/__init__.py b/discord/__init__.py index 780460dc51f6..c206f650f66f 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -71,6 +71,7 @@ from .automod import * from .poll import * from .soundboard import * +from .subscription import * class VersionInfo(NamedTuple): diff --git a/discord/client.py b/discord/client.py index 6ee4a003d33f..ff02bf7b6f00 100644 --- a/discord/client.py +++ b/discord/client.py @@ -119,6 +119,7 @@ from .voice_client import VoiceProtocol from .audit_logs import AuditLogEntry from .poll import PollAnswer + from .subscription import Subscription # fmt: off @@ -1373,6 +1374,18 @@ async def wait_for( ) -> Union[str, bytes]: ... + # Entitlements + @overload + async def wait_for( + self, + event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'], + /, + *, + check: Optional[Callable[[Entitlement], bool]], + timeout: Optional[float] = None, + ) -> Entitlement: + ... + # Guilds @overload @@ -1781,6 +1794,18 @@ async def wait_for( ) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]: ... + # Subscriptions + @overload + async def wait_for( + self, + event: Literal['subscription_create', 'subscription_update', 'subscription_delete'], + /, + *, + check: Optional[Callable[[Subscription], bool]], + timeout: Optional[float] = None, + ) -> Subscription: + ... + # Threads @overload async def wait_for( diff --git a/discord/enums.py b/discord/enums.py index 9000c8c04576..3aecfc92b654 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -75,6 +75,7 @@ 'EntitlementOwnerType', 'PollLayoutType', 'VoiceChannelEffectAnimationType', + 'SubscriptionStatus', ) @@ -847,6 +848,12 @@ class VoiceChannelEffectAnimationType(Enum): basic = 1 +class SubscriptionStatus(Enum): + active = 0 + ending = 1 + inactive = 2 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/http.py b/discord/http.py index 3c1eacb61fb8..8bd7a9804aed 100644 --- a/discord/http.py +++ b/discord/http.py @@ -94,6 +94,7 @@ poll, voice, soundboard, + subscription, ) from .types.snowflake import Snowflake, SnowflakeList @@ -2699,6 +2700,49 @@ def end_poll(self, channel_id: Snowflake, message_id: Snowflake) -> Response[mes ) ) + # Subscriptions + + def list_sku_subscriptions( + self, + sku_id: Snowflake, + before: Optional[Snowflake] = None, + after: Optional[Snowflake] = None, + limit: Optional[int] = None, + user_id: Optional[Snowflake] = None, + ) -> Response[List[subscription.Subscription]]: + params = {} + + if before is not None: + params['before'] = before + + if after is not None: + params['after'] = after + + if limit is not None: + params['limit'] = limit + + if user_id is not None: + params['user_id'] = user_id + + return self.request( + Route( + 'GET', + '/skus/{sku_id}/subscriptions', + sku_id=sku_id, + ), + params=params, + ) + + def get_sku_subscription(self, sku_id: Snowflake, subscription_id: Snowflake) -> Response[subscription.Subscription]: + return self.request( + Route( + 'GET', + '/skus/{sku_id}/subscriptions/{subscription_id}', + sku_id=sku_id, + subscription_id=subscription_id, + ) + ) + # Misc async def get_bot_gateway(self) -> Tuple[int, str]: diff --git a/discord/sku.py b/discord/sku.py index e8780399ca6a..9ad325366da4 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -25,16 +25,18 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import AsyncIterator, Optional, TYPE_CHECKING from . import utils -from .errors import MissingApplicationID from .enums import try_enum, SKUType, EntitlementType from .flags import SKUFlags +from .object import Object +from .subscription import Subscription if TYPE_CHECKING: from datetime import datetime + from .abc import SnowflakeTime, Snowflake from .guild import Guild from .state import ConnectionState from .types.sku import ( @@ -100,6 +102,149 @@ def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the sku's creation time in UTC.""" return utils.snowflake_time(self.id) + async def fetch_subscription(self, subscription_id: int, /) -> Subscription: + """|coro| + + Retrieves a :class:`.Subscription` with the specified ID. + + .. versionadded:: 2.5 + + Parameters + ----------- + subscription_id: :class:`int` + The subscription's ID to fetch from. + + Raises + ------- + NotFound + An subscription with this ID does not exist. + HTTPException + Fetching the subscription failed. + + Returns + -------- + :class:`.Subscription` + The subscription you requested. + """ + data = await self._state.http.get_sku_subscription(self.id, subscription_id) + return Subscription(data=data, state=self._state) + + async def subscriptions( + self, + *, + limit: Optional[int] = 50, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + user: Snowflake, + ) -> AsyncIterator[Subscription]: + """Retrieves an :term:`asynchronous iterator` of the :class:`.Subscription` that SKU has. + + .. versionadded:: 2.5 + + Examples + --------- + + Usage :: + + async for subscription in sku.subscriptions(limit=100): + print(subscription.user_id, subscription.current_period_end) + + Flattening into a list :: + + subscriptions = [subscription async for subscription in sku.subscriptions(limit=100)] + # subscriptions is now a list of Subscription... + + All parameters are optional. + + Parameters + ----------- + limit: Optional[:class:`int`] + The number of subscriptions to retrieve. If ``None``, it retrieves every subscription for this SKU. + Note, however, that this would make it a slow operation. Defaults to ``100``. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve subscriptions before this date or entitlement. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve subscriptions after this date or entitlement. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + user: :class:`~discord.abc.Snowflake` + The user to filter by. + + Raises + ------- + HTTPException + Fetching the subscriptions failed. + TypeError + Both ``after`` and ``before`` were provided, as Discord does not + support this type of pagination. + + Yields + -------- + :class:`.Subscription` + The subscription with the SKU. + """ + + if before is not None and after is not None: + raise TypeError('subscriptions pagination does not support both before and after') + + # This endpoint paginates in ascending order. + endpoint = self._state.http.list_sku_subscriptions + + async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): + before_id = before.id if before else None + data = await endpoint(self.id, before=before_id, limit=retrieve, user_id=user.id) + + if data: + if limit is not None: + limit -= len(data) + + before = Object(id=int(data[0]['id'])) + + return data, before, limit + + async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]): + after_id = after.id if after else None + data = await endpoint( + self.id, + after=after_id, + limit=retrieve, + user_id=user.id, + ) + + if data: + if limit is not None: + limit -= len(data) + + after = Object(id=int(data[-1]['id'])) + + return data, after, limit + + if isinstance(before, datetime): + before = Object(id=utils.time_snowflake(before, high=False)) + if isinstance(after, datetime): + after = Object(id=utils.time_snowflake(after, high=True)) + + if before: + strategy, state = _before_strategy, before + else: + strategy, state = _after_strategy, after + + while True: + retrieve = 100 if limit is None else min(limit, 100) + if retrieve < 1: + return + + data, state, limit = await strategy(retrieve, state, limit) + + # Terminate loop on next iteration; there's no data left after this + if len(data) < 1000: + limit = 0 + + for e in data: + yield Subscription(data=e, state=self._state) + class Entitlement: """Represents an entitlement from user or guild which has been granted access to a premium offering. @@ -190,17 +335,12 @@ async def consume(self) -> None: Raises ------- - MissingApplicationID - The application ID could not be found. NotFound The entitlement could not be found. HTTPException Consuming the entitlement failed. """ - if self.application_id is None: - raise MissingApplicationID - await self._state.http.consume_entitlement(self.application_id, self.id) async def delete(self) -> None: @@ -210,15 +350,10 @@ async def delete(self) -> None: Raises ------- - MissingApplicationID - The application ID could not be found. NotFound The entitlement could not be found. HTTPException Deleting the entitlement failed. """ - if self.application_id is None: - raise MissingApplicationID - await self._state.http.delete_entitlement(self.application_id, self.id) diff --git a/discord/state.py b/discord/state.py index 91fd915bf6bc..83628af3243f 100644 --- a/discord/state.py +++ b/discord/state.py @@ -79,6 +79,8 @@ from .audit_logs import AuditLogEntry from ._types import ClientT from .soundboard import SoundboardSound +from .subscription import Subscription + if TYPE_CHECKING: from .abc import PrivateChannel @@ -1736,6 +1738,18 @@ def parse_message_poll_vote_remove(self, data: gw.PollVoteActionEvent) -> None: if poll: self.dispatch('poll_vote_remove', user, poll.get_answer(raw.answer_id)) + def parse_subscription_create(self, data: gw.SubscriptionCreateEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_create', subscription) + + def parse_subscription_update(self, data: gw.SubscriptionUpdateEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_update', subscription) + + def parse_subscription_delete(self, data: gw.SubscriptionDeleteEvent) -> None: + subscription = Subscription(data=data, state=self) + self.dispatch('subscription_delete', subscription) + def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]: if isinstance(channel, (TextChannel, Thread, VoiceChannel)): return channel.guild.get_member(user_id) diff --git a/discord/subscription.py b/discord/subscription.py new file mode 100644 index 000000000000..d861615abca3 --- /dev/null +++ b/discord/subscription.py @@ -0,0 +1,103 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import datetime +from typing import List, Optional, TYPE_CHECKING + +from . import utils +from .mixins import Hashable +from .enums import try_enum, SubscriptionStatus + +if TYPE_CHECKING: + from .state import ConnectionState + from .types.subscription import Subscription as SubscriptionPayload + from .user import User + +__all__ = ('Subscription',) + + +class Subscription(Hashable): + """Represents a Discord subscription. + + .. versionadded:: 2.5 + + Attributes + ----------- + id: :class:`int` + The subscription's ID. + user_id: :class:`int` + The ID of the user that is subscribed. + sku_ids: List[:class:`int`] + The IDs of the SKUs that the user subscribed to. + entitlement_ids: List[:class:`int`] + The IDs of the entitlements granted for this subscription. + current_period_start: :class:`datetime.datetime` + When the current billing period started. + current_period_end: :class:`datetime.datetime` + When the current billing period ends. + status: :class:`SubscriptionStatus` + The status of the subscription. + canceled_at: Optional[:class:`datetime.datetime`] + When the subscription was canceled. + This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`. + """ + + __slots__ = ( + '_state', + 'id', + 'user_id', + 'sku_ids', + 'entitlement_ids', + 'current_period_start', + 'current_period_end', + 'status', + 'canceled_at', + ) + + def __init__(self, *, state: ConnectionState, data: SubscriptionPayload): + self._state = state + + self.id: int = int(data['id']) + self.user_id: int = int(data['user_id']) + self.sku_ids: List[int] = list(map(int, data['sku_ids'])) + self.entitlement_ids: List[int] = list(map(int, data['entitlement_ids'])) + self.current_period_start: datetime.datetime = utils.parse_time(data['current_period_start']) + self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end']) + self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status']) + self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at']) + + def __repr__(self) -> str: + return f'' + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the subscription's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user that is subscribed.""" + return self._state.get_user(self.user_id) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 974ceb20460b..6261c70dd864 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -46,6 +46,7 @@ from .scheduled_event import GuildScheduledEvent from .audit_log import AuditLogEntry from .soundboard import SoundboardSound +from .subscription import Subscription class SessionStartLimit(TypedDict): @@ -372,3 +373,6 @@ class PollVoteActionEvent(TypedDict): message_id: Snowflake guild_id: NotRequired[Snowflake] answer_id: int + + +SubscriptionCreateEvent = SubscriptionUpdateEvent = SubscriptionDeleteEvent = Subscription diff --git a/discord/types/subscription.py b/discord/types/subscription.py new file mode 100644 index 000000000000..bb707afce15f --- /dev/null +++ b/discord/types/subscription.py @@ -0,0 +1,42 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import List, Literal, Optional, TypedDict + +from .snowflake import Snowflake + +SubscriptionStatus = Literal[0, 1, 2] + + +class Subscription(TypedDict): + id: Snowflake + user_id: Snowflake + sku_ids: List[Snowflake] + entitlement_ids: List[Snowflake] + current_period_start: str + current_period_end: str + status: SubscriptionStatus + canceled_at: Optional[str] diff --git a/docs/api.rst b/docs/api.rst index c4feaa246501..4b88e4871925 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1356,6 +1356,37 @@ Stages :param after: The stage instance after the update. :type after: :class:`StageInstance` + +Subscriptions +~~~~~~~~~~~~~ + +.. function:: on_subscription_create(subscription) + + Called when a subscription is created. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was created. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_update(subscription) + + Called when a subscription is updated. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was updated. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_delete(subscription) + + Called when a subscription is deleted. + + .. versionadded:: 2.5 + + :param subscription: The subscription that was deleted. + :type subscription: :class:`Subscription` + Threads ~~~~~~~~ @@ -3754,6 +3785,25 @@ of :class:`enum.Enum`. The standard animation. +.. class:: SubscriptionStatus + + Represents the status of an subscription. + + .. versionadded:: 2.5 + + .. attribute:: active + + The subscription is active. + + .. attribute:: ending + + The subscription is active but will not renew. + + .. attribute:: inactive + + The subscription is inactive and not being charged. + + .. _discord-api-audit-logs: Audit Log Data @@ -5151,6 +5201,14 @@ Entitlement .. autoclass:: Entitlement() :members: +Subscription +~~~~~~~~~~~~ + +.. attributetable:: Subscription + +.. autoclass:: Subscription() + :members: + RawMessageDeleteEvent ~~~~~~~~~~~~~~~~~~~~~~~ From 9d7c253535fa3b2060a0769df6d19230ef45f1e2 Mon Sep 17 00:00:00 2001 From: iyad-f <128908811+iyad-f@users.noreply.github.com> Date: Thu, 10 Oct 2024 03:41:27 +0530 Subject: [PATCH 123/354] Add missing error for Message.edit --- discord/message.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/message.py b/discord/message.py index 76127f869e48..c6ef052ee498 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1102,6 +1102,8 @@ async def edit( Forbidden Tried to suppress a message without permissions or edited a message's content or embed that isn't yours. + NotFound + This message does not exist. TypeError You specified both ``embed`` and ``embeds`` @@ -2529,6 +2531,8 @@ async def edit( Forbidden Tried to suppress a message without permissions or edited a message's content or embed that isn't yours. + NotFound + This message does not exist. TypeError You specified both ``embed`` and ``embeds`` From 20c543f6729f081dd7963582387e0bac3ea03e9d Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:21:59 +0200 Subject: [PATCH 124/354] Add support for message call --- discord/message.py | 77 +++++++++++++++++++++++++++++++++++++++- discord/types/message.py | 6 ++++ discord/utils.py | 52 +++++++++++++++++++++++++++ docs/api.rst | 8 +++++ 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index c6ef052ee498..12a4c90cec6f 100644 --- a/discord/message.py +++ b/discord/message.py @@ -76,6 +76,7 @@ MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, MessageInteractionMetadata as MessageInteractionMetadataPayload, + CallMessage as CallMessagePayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -112,6 +113,7 @@ 'MessageApplication', 'RoleSubscriptionInfo', 'MessageInteractionMetadata', + 'CallMessage', ) @@ -810,6 +812,51 @@ def cover(self) -> Optional[Asset]: return None +class CallMessage: + """Represents a message's call data in a private channel from a :class:`~discord.Message`. + + .. versionadded:: 2.5 + + Attributes + ----------- + ended_timestamp: Optional[:class:`datetime.datetime`] + The timestamp the call has ended. + participants: List[:class:`User`] + A list of users that participated in the call. + """ + + __slots__ = ('_message', 'ended_timestamp', 'participants') + + def __repr__(self) -> str: + return f'' + + def __init__(self, *, state: ConnectionState, message: Message, data: CallMessagePayload): + self._message: Message = message + self.ended_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('ended_timestamp')) + self.participants: List[User] = [] + + for user_id in data['participants']: + user_id = int(user_id) + if user_id == self._message.author.id: + self.participants.append(self._message.author) # type: ignore # can't be a Member here + else: + user = state.get_user(user_id) + if user is not None: + self.participants.append(user) + + @property + def duration(self) -> datetime.timedelta: + """:class:`datetime.timedelta`: The duration the call has lasted or is already ongoing.""" + if self.ended_timestamp is None: + return utils.utcnow() - self._message.created_at + else: + return self.ended_timestamp - self._message.created_at + + def is_ended(self) -> bool: + """:class:`bool`: Whether the call is ended or not.""" + return self.ended_timestamp is not None + + class RoleSubscriptionInfo: """Represents a message's role subscription information. @@ -1770,6 +1817,10 @@ class Message(PartialMessage, Hashable): The poll attached to this message. .. versionadded:: 2.4 + call: Optional[:class:`CallMessage`] + The call associated with this message. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -1806,6 +1857,7 @@ class Message(PartialMessage, Hashable): 'position', 'interaction_metadata', 'poll', + 'call', ) if TYPE_CHECKING: @@ -1931,7 +1983,7 @@ def __init__( else: self.role_subscription = RoleSubscriptionInfo(role_subscription) - for handler in ('author', 'member', 'mentions', 'mention_roles', 'components'): + for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: getattr(self, f'_handle_{handler}')(data[handler]) except KeyError: @@ -2117,6 +2169,13 @@ def _handle_interaction(self, data: MessageInteractionPayload): def _handle_interaction_metadata(self, data: MessageInteractionMetadataPayload): self.interaction_metadata = MessageInteractionMetadata(state=self._state, guild=self.guild, data=data) + def _handle_call(self, data: CallMessagePayload): + self.call: Optional[CallMessage] + if data is not None: + self.call = CallMessage(state=self._state, message=self, data=data) + else: + self.call = None + def _rebind_cached_references( self, new_guild: Guild, @@ -2421,6 +2480,22 @@ def system_content(self) -> str: if self.type is MessageType.guild_incident_report_false_alarm: return f'{self.author.name} reported a false alarm in {self.guild}.' + if self.type is MessageType.call: + call_ended = self.call.ended_timestamp is not None # type: ignore # call can't be None here + missed = self._state.user not in self.call.participants # type: ignore # call can't be None here + + if call_ended: + duration = utils._format_call_duration(self.call.duration) # type: ignore # call can't be None here + if missed: + return 'You missed a call from {0.author.name} that lasted {1}.'.format(self, duration) + else: + return '{0.author.name} started a call that lasted {1}.'.format(self, duration) + else: + if missed: + return '{0.author.name} started a call. \N{EM DASH} Join the call'.format(self) + else: + return '{0.author.name} started a call.'.format(self) + # Fallback for unknown message types return '' diff --git a/discord/types/message.py b/discord/types/message.py index bdb3f10ef9e6..995dc8b8b5cc 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -116,6 +116,11 @@ class RoleSubscriptionData(TypedDict): is_renewal: bool +class CallMessage(TypedDict): + participants: SnowflakeList + ended_timestamp: NotRequired[Optional[str]] + + MessageType = Literal[ 0, 1, @@ -187,6 +192,7 @@ class Message(PartialMessage): position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] thread: NotRequired[Thread] + call: NotRequired[CallMessage] AllowedMentionType = Literal['roles', 'users', 'everyone'] diff --git a/discord/utils.py b/discord/utils.py index cb7d662b62ec..5d898b38bd34 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1468,3 +1468,55 @@ def decompress(self, data: bytes, /) -> str | None: return msg.decode('utf-8') _ActiveDecompressionContext: Type[_DecompressionContext] = _ZlibDecompressionContext + + +def _format_call_duration(duration: datetime.timedelta) -> str: + seconds = duration.total_seconds() + + minutes_s = 60 + hours_s = minutes_s * 60 + days_s = hours_s * 24 + # Discord uses approx. 1/12 of 365.25 days (avg. days per year) + months_s = days_s * 30.4375 + years_s = months_s * 12 + + threshold_s = 45 + threshold_m = 45 + threshold_h = 21.5 + threshold_d = 25.5 + threshold_M = 10.5 + + if seconds < threshold_s: + formatted = "a few seconds" + elif seconds < (threshold_m * minutes_s): + minutes = round(seconds / minutes_s) + if minutes == 1: + formatted = "a minute" + else: + formatted = f"{minutes} minutes" + elif seconds < (threshold_h * hours_s): + hours = round(seconds / hours_s) + if hours == 1: + formatted = "an hour" + else: + formatted = f"{hours} hours" + elif seconds < (threshold_d * days_s): + days = round(seconds / days_s) + if days == 1: + formatted = "a day" + else: + formatted = f"{days} days" + elif seconds < (threshold_M * months_s): + months = round(seconds / months_s) + if months == 1: + formatted = "a month" + else: + formatted = f"{months} months" + else: + years = round(seconds / years_s) + if years == 1: + formatted = "a year" + else: + formatted = f"{years} years" + + return formatted diff --git a/docs/api.rst b/docs/api.rst index 4b88e4871925..3531dde06c0c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5626,6 +5626,14 @@ PollMedia .. autoclass:: PollMedia :members: +CallMessage +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: CallMessage + +.. autoclass:: CallMessage() + :members: + Exceptions ------------ From a5f9350ff2a58738035194136d4a3bbde38b07a6 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:22:52 +0200 Subject: [PATCH 125/354] Add category parameter to abc.GuildChannel.clone --- discord/abc.py | 19 +++++++++++++++++- discord/channel.py | 48 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 7f10811c4394..57c26ad90f86 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1005,11 +1005,15 @@ async def _clone_impl( base_attrs: Dict[str, Any], *, name: Optional[str] = None, + category: Optional[CategoryChannel] = None, reason: Optional[str] = None, ) -> Self: base_attrs['permission_overwrites'] = [x._asdict() for x in self._overwrites] base_attrs['parent_id'] = self.category_id base_attrs['name'] = name or self.name + if category is not None: + base_attrs['parent_id'] = category.id + guild_id = self.guild.id cls = self.__class__ data = await self._state.http.create_channel(guild_id, self.type.value, reason=reason, **base_attrs) @@ -1019,7 +1023,13 @@ async def _clone_impl( self.guild._channels[obj.id] = obj # type: ignore # obj is a GuildChannel return obj - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> Self: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> Self: """|coro| Clones this channel. This creates a channel with the same properties @@ -1029,11 +1039,18 @@ async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = Non .. versionadded:: 1.1 + .. versionchanged:: 2.5 + + The ``category`` keyword-only parameter was added. + Parameters ------------ name: Optional[:class:`str`] The name of the new channel. If not provided, defaults to this channel name. + category: Optional[:class:`~discord.CategoryChannel`] + The category the new channel belongs to. + This parameter is ignored if cloning a category channel. reason: Optional[:class:`str`] The reason for cloning this channel. Shows up on the audit log. diff --git a/discord/channel.py b/discord/channel.py index 9789b7b3fecc..b8858f356693 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -525,7 +525,13 @@ async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optiona return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> TextChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> TextChannel: return await self._clone_impl( { 'topic': self.topic, @@ -535,6 +541,7 @@ async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = Non 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, }, name=name, + category=category, reason=reason, ) @@ -1499,6 +1506,18 @@ def type(self) -> Literal[ChannelType.voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.voice + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> VoiceChannel: + return await self._clone_impl( + {'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, category=category, reason=reason + ) + @overload async def edit(self) -> None: ... @@ -1769,6 +1788,16 @@ def type(self) -> Literal[ChannelType.stage_voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.stage_voice + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> StageChannel: + return await self._clone_impl({}, name=name, category=category, reason=reason) + @property def instance(self) -> Optional[StageInstance]: """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. @@ -2046,7 +2075,13 @@ def is_nsfw(self) -> bool: return self.nsfw @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> CategoryChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel] = None, + reason: Optional[str] = None, + ) -> CategoryChannel: return await self._clone_impl({'nsfw': self.nsfw}, name=name, reason=reason) @overload @@ -2563,7 +2598,13 @@ def is_media(self) -> bool: return self._type == ChannelType.media.value @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> ForumChannel: + async def clone( + self, + *, + name: Optional[str] = None, + category: Optional[CategoryChannel], + reason: Optional[str] = None, + ) -> ForumChannel: base = { 'topic': self.topic, 'rate_limit_per_user': self.slowmode_delay, @@ -2582,6 +2623,7 @@ async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = Non return await self._clone_impl( base, name=name, + category=category, reason=reason, ) From 04b8f385b1fce165077588c60c6374f6c1760d3c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Fri, 11 Oct 2024 04:02:58 +0530 Subject: [PATCH 126/354] Remove leftover print statement --- discord/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 8bd7a9804aed..02684e3934ef 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2584,7 +2584,6 @@ def delete_soundboard_sound(self, guild_id: Snowflake, sound_id: Snowflake, *, r def send_soundboard_sound(self, channel_id: Snowflake, **payload: Any) -> Response[None]: valid_keys = ('sound_id', 'source_guild_id') payload = {k: v for k, v in payload.items() if k in valid_keys} - print(payload) return self.request( (Route('POST', '/channels/{channel_id}/send-soundboard-sound', channel_id=channel_id)), json=payload ) From b3141db6e9e95280f35c1376ae74313c767a9f71 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:43:07 +0200 Subject: [PATCH 127/354] Add support for messages with type purchase_notification --- discord/enums.py | 1 + discord/message.py | 75 ++++++++++++++++++++++++++++++++++++++++ discord/types/message.py | 15 ++++++++ docs/api.rst | 22 ++++++++++++ 4 files changed, 113 insertions(+) diff --git a/discord/enums.py b/discord/enums.py index 3aecfc92b654..1312f0320fa2 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -258,6 +258,7 @@ class MessageType(Enum): guild_incident_alert_mode_disabled = 37 guild_incident_report_raid = 38 guild_incident_report_false_alarm = 39 + purchase_notification = 44 class SpeakingState(Enum): diff --git a/discord/message.py b/discord/message.py index 12a4c90cec6f..6ce7cf9e9757 100644 --- a/discord/message.py +++ b/discord/message.py @@ -77,6 +77,8 @@ RoleSubscriptionData as RoleSubscriptionDataPayload, MessageInteractionMetadata as MessageInteractionMetadataPayload, CallMessage as CallMessagePayload, + PurchaseNotificationResponse as PurchaseNotificationResponsePayload, + GuildProductPurchase as GuildProductPurchasePayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -114,6 +116,8 @@ 'RoleSubscriptionInfo', 'MessageInteractionMetadata', 'CallMessage', + 'GuildProductPurchase', + 'PurchaseNotification', ) @@ -890,6 +894,59 @@ def __init__(self, data: RoleSubscriptionDataPayload) -> None: self.is_renewal: bool = data['is_renewal'] +class GuildProductPurchase: + """Represents a message's guild product that the user has purchased. + + .. versionadded:: 2.5 + + Attributes + ----------- + listing_id: :class:`int` + The ID of the listing that the user has purchased. + product_name: :class:`str` + The name of the product that the user has purchased. + """ + + __slots__ = ('listing_id', 'product_name') + + def __init__(self, data: GuildProductPurchasePayload) -> None: + self.listing_id: int = int(data['listing_id']) + self.product_name: str = data['product_name'] + + def __hash__(self) -> int: + return self.listing_id >> 22 + + def __eq__(self, other: object) -> bool: + return isinstance(other, GuildProductPurchase) and other.listing_id == self.listing_id + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + +class PurchaseNotification: + """Represents a message's purchase notification data. + + This is currently only attached to messages of type :attr:`MessageType.purchase_notification`. + + .. versionadded:: 2.5 + + Attributes + ----------- + guild_product_purchase: Optional[:class:`GuildProductPurchase`] + The guild product purchase that prompted the message. + """ + + __slots__ = ('_type', 'guild_product_purchase') + + def __init__(self, data: PurchaseNotificationResponsePayload) -> None: + self._type: int = data['type'] + + self.guild_product_purchase: Optional[GuildProductPurchase] = None + guild_product_purchase = data.get('guild_product_purchase') + if guild_product_purchase is not None: + self.guild_product_purchase = GuildProductPurchase(guild_product_purchase) + + class PartialMessage(Hashable): """Represents a partial message to aid with working messages when only a message and channel ID are present. @@ -1820,6 +1877,10 @@ class Message(PartialMessage, Hashable): call: Optional[:class:`CallMessage`] The call associated with this message. + .. versionadded:: 2.5 + purchase_notification: Optional[:class:`PurchaseNotification`] + The data of the purchase notification that prompted this :attr:`MessageType.purchase_notification` message. + .. versionadded:: 2.5 """ @@ -1858,6 +1919,7 @@ class Message(PartialMessage, Hashable): 'interaction_metadata', 'poll', 'call', + 'purchase_notification', ) if TYPE_CHECKING: @@ -1983,6 +2045,14 @@ def __init__( else: self.role_subscription = RoleSubscriptionInfo(role_subscription) + self.purchase_notification: Optional[PurchaseNotification] = None + try: + purchase_notification = data['purchase_notification'] + except KeyError: + pass + else: + self.purchase_notification = PurchaseNotification(purchase_notification) + for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: getattr(self, f'_handle_{handler}')(data[handler]) @@ -2496,6 +2566,11 @@ def system_content(self) -> str: else: return '{0.author.name} started a call.'.format(self) + if self.type is MessageType.purchase_notification and self.purchase_notification is not None: + guild_product_purchase = self.purchase_notification.guild_product_purchase + if guild_product_purchase is not None: + return f'{self.author.name} has purchased {guild_product_purchase.product_name}!' + # Fallback for unknown message types return '' diff --git a/discord/types/message.py b/discord/types/message.py index 995dc8b8b5cc..c1972541a254 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -116,6 +116,19 @@ class RoleSubscriptionData(TypedDict): is_renewal: bool +PurchaseNotificationResponseType = Literal[0] + + +class GuildProductPurchase(TypedDict): + listing_id: Snowflake + product_name: str + + +class PurchaseNotificationResponse(TypedDict): + type: PurchaseNotificationResponseType + guild_product_purchase: Optional[GuildProductPurchase] + + class CallMessage(TypedDict): participants: SnowflakeList ended_timestamp: NotRequired[Optional[str]] @@ -156,6 +169,7 @@ class CallMessage(TypedDict): 37, 38, 39, + 44, ] @@ -193,6 +207,7 @@ class Message(PartialMessage): role_subscription_data: NotRequired[RoleSubscriptionData] thread: NotRequired[Thread] call: NotRequired[CallMessage] + purchase_notification: NotRequired[PurchaseNotificationResponse] AllowedMentionType = Literal['roles', 'users', 'everyone'] diff --git a/docs/api.rst b/docs/api.rst index 3531dde06c0c..6f728d56b3c6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1881,6 +1881,12 @@ of :class:`enum.Enum`. .. versionadded:: 2.4 + .. attribute:: purchase_notification + + The system message sent when a purchase is made in the guild. + + .. versionadded:: 2.5 + .. class:: UserFlags Represents Discord User flags. @@ -5418,6 +5424,22 @@ RoleSubscriptionInfo .. autoclass:: RoleSubscriptionInfo :members: +PurchaseNotification +~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PurchaseNotification + +.. autoclass:: PurchaseNotification() + :members: + +GuildProductPurchase ++++++++++++++++++++++ + +.. attributetable:: GuildProductPurchase + +.. autoclass:: GuildProductPurchase() + :members: + Intents ~~~~~~~~~~ From 48cf500e092a5a315b855df60e9122bcdb6c9cb7 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:38:27 +0530 Subject: [PATCH 128/354] Fix Message.system_content for role_subscription_purchase renewal type --- discord/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index 6ce7cf9e9757..cefec9778238 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2516,10 +2516,10 @@ def system_content(self) -> str: return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!' if self.type is MessageType.role_subscription_purchase and self.role_subscription is not None: - # TODO: figure out how the message looks like for is_renewal: true total_months = self.role_subscription.total_months_subscribed months = '1 month' if total_months == 1 else f'{total_months} months' - return f'{self.author.name} joined {self.role_subscription.tier_name} and has been a subscriber of {self.guild} for {months}!' + action = 'renewed' if self.role_subscription.is_renewal else 'joined' + return f'{self.author.name} {action} **{self.role_subscription.tier_name}** and has been a subscriber of {self.guild} for {months}!' if self.type is MessageType.stage_start: return f'{self.author.name} started **{self.content}**.' From b11f19a39766206d2a8185150a6af865b3b88b71 Mon Sep 17 00:00:00 2001 From: Sherlock Date: Sat, 12 Oct 2024 03:31:47 +0800 Subject: [PATCH 129/354] Add proxy support for get_from_cdn --- discord/http.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 02684e3934ef..fbaf447aa415 100644 --- a/discord/http.py +++ b/discord/http.py @@ -777,7 +777,15 @@ async def request( raise RuntimeError('Unreachable code in HTTP handling') async def get_from_cdn(self, url: str) -> bytes: - async with self.__session.get(url) as resp: + kwargs = {} + + # Proxy support + if self.proxy is not None: + kwargs['proxy'] = self.proxy + if self.proxy_auth is not None: + kwargs['proxy_auth'] = self.proxy_auth + + async with self.__session.get(url, **kwargs) as resp: if resp.status == 200: return await resp.read() elif resp.status == 404: From 99a7093c340d2b768d8c7c8e216e13988ba64ec0 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:34:18 +0200 Subject: [PATCH 130/354] Add support for message forwarding Co-authored-by: Red Magnos Co-authored-by: MCausc78 Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/enums.py | 6 ++ discord/message.py | 223 +++++++++++++++++++++++++++++++++++++-- discord/types/message.py | 18 ++++ docs/api.rst | 26 +++++ 4 files changed, 266 insertions(+), 7 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 1312f0320fa2..5a361b2259cf 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -220,6 +220,12 @@ def __str__(self) -> str: return self.name +class MessageReferenceType(Enum): + default = 0 + reply = 0 + forward = 1 + + class MessageType(Enum): default = 0 recipient_add = 1 diff --git a/discord/message.py b/discord/message.py index cefec9778238..44025f460fee 100644 --- a/discord/message.py +++ b/discord/message.py @@ -32,6 +32,7 @@ from typing import ( Dict, TYPE_CHECKING, + Literal, Sequence, Union, List, @@ -49,7 +50,7 @@ from .reaction import Reaction from .emoji import Emoji from .partial_emoji import PartialEmoji -from .enums import InteractionType, MessageType, ChannelType, try_enum +from .enums import InteractionType, MessageReferenceType, MessageType, ChannelType, try_enum from .errors import HTTPException from .components import _component_factory from .embeds import Embed @@ -72,6 +73,7 @@ Message as MessagePayload, Attachment as AttachmentPayload, MessageReference as MessageReferencePayload, + MessageSnapshot as MessageSnapshotPayload, MessageApplication as MessageApplicationPayload, MessageActivity as MessageActivityPayload, RoleSubscriptionData as RoleSubscriptionDataPayload, @@ -111,6 +113,7 @@ 'PartialMessage', 'MessageInteraction', 'MessageReference', + 'MessageSnapshot', 'DeletedReferencedMessage', 'MessageApplication', 'RoleSubscriptionInfo', @@ -464,6 +467,133 @@ def guild_id(self) -> Optional[int]: return self._parent.guild_id +class MessageSnapshot: + """Represents a message snapshot attached to a forwarded message. + + .. versionadded:: 2.5 + + Attributes + ----------- + type: :class:`MessageType` + The type of the forwarded message. + content: :class:`str` + The actual contents of the forwarded message. + embeds: List[:class:`Embed`] + A list of embeds the forwarded message has. + attachments: List[:class:`Attachment`] + A list of attachments given to the forwarded message. + created_at: :class:`datetime.datetime` + The forwarded message's time of creation. + flags: :class:`MessageFlags` + Extra features of the the message snapshot. + stickers: List[:class:`StickerItem`] + A list of sticker items given to the message. + components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]] + A list of components in the message. + """ + + __slots__ = ( + '_cs_raw_channel_mentions', + '_cs_cached_message', + '_cs_raw_mentions', + '_cs_raw_role_mentions', + '_edited_timestamp', + 'attachments', + 'content', + 'embeds', + 'flags', + 'created_at', + 'type', + 'stickers', + 'components', + '_state', + ) + + @classmethod + def _from_value( + cls, + state: ConnectionState, + message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]], + ) -> List[Self]: + if not message_snapshots: + return [] + + return [cls(state, snapshot['message']) for snapshot in message_snapshots] + + def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): + self.type: MessageType = try_enum(MessageType, data['type']) + self.content: str = data['content'] + self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] + self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']] + self.created_at: datetime.datetime = utils.parse_time(data['timestamp']) + self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) + self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) + self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('stickers_items', [])] + + self.components: List[MessageComponentType] = [] + for component_data in data.get('components', []): + component = _component_factory(component_data) + if component is not None: + self.components.append(component) + + self._state: ConnectionState = state + + def __repr__(self) -> str: + name = self.__class__.__name__ + return f'<{name} type={self.type!r} created_at={self.created_at!r} flags={self.flags!r}>' + + @utils.cached_slot_property('_cs_raw_mentions') + def raw_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of user IDs matched with + the syntax of ``<@user_id>`` in the message content. + + This allows you to receive the user IDs of mentioned users + even in a private message context. + """ + return [int(x) for x in re.findall(r'<@!?([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_raw_channel_mentions') + def raw_channel_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of channel IDs matched with + the syntax of ``<#channel_id>`` in the message content. + """ + return [int(x) for x in re.findall(r'<#([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_raw_role_mentions') + def raw_role_mentions(self) -> List[int]: + """List[:class:`int`]: A property that returns an array of role IDs matched with + the syntax of ``<@&role_id>`` in the message content. + """ + return [int(x) for x in re.findall(r'<@&([0-9]{15,20})>', self.content)] + + @utils.cached_slot_property('_cs_cached_message') + def cached_message(self) -> Optional[Message]: + """Optional[:class:`Message`]: Returns the cached message this snapshot points to, if any.""" + state = self._state + return ( + utils.find( + lambda m: ( + m.created_at == self.created_at + and m.edited_at == self.edited_at + and m.content == self.content + and m.embeds == self.embeds + and m.components == self.components + and m.stickers == self.stickers + and m.attachments == self.attachments + and m.flags == self.flags + ), + reversed(state._messages), + ) + if state._messages + else None + ) + + @property + def edited_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the forwarded message.""" + return self._edited_timestamp + + class MessageReference: """Represents a reference to a :class:`~discord.Message`. @@ -474,6 +604,10 @@ class MessageReference: Attributes ----------- + type: :class:`MessageReferenceType` + The type of message reference. + + .. versionadded:: 2.5 message_id: Optional[:class:`int`] The id of the message referenced. channel_id: :class:`int` @@ -498,10 +632,19 @@ class MessageReference: .. versionadded:: 1.6 """ - __slots__ = ('message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state') + __slots__ = ('type', 'message_id', 'channel_id', 'guild_id', 'fail_if_not_exists', 'resolved', '_state') - def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int] = None, fail_if_not_exists: bool = True): + def __init__( + self, + *, + message_id: int, + channel_id: int, + guild_id: Optional[int] = None, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ): self._state: Optional[ConnectionState] = None + self.type: MessageReferenceType = type self.resolved: Optional[Union[Message, DeletedReferencedMessage]] = None self.message_id: Optional[int] = message_id self.channel_id: int = channel_id @@ -511,6 +654,7 @@ def __init__(self, *, message_id: int, channel_id: int, guild_id: Optional[int] @classmethod def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Self: self = cls.__new__(cls) + self.type = try_enum(MessageReferenceType, data.get('type', 0)) self.message_id = utils._get_as_snowflake(data, 'message_id') self.channel_id = int(data['channel_id']) self.guild_id = utils._get_as_snowflake(data, 'guild_id') @@ -520,7 +664,13 @@ def with_state(cls, state: ConnectionState, data: MessageReferencePayload) -> Se return self @classmethod - def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = True) -> Self: + def from_message( + cls, + message: PartialMessage, + *, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ) -> Self: """Creates a :class:`MessageReference` from an existing :class:`~discord.Message`. .. versionadded:: 1.6 @@ -534,6 +684,10 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 + type: :class:`~discord.MessageReferenceType` + The type of message reference this is. + + .. versionadded:: 2.5 Returns ------- @@ -545,6 +699,7 @@ def from_message(cls, message: PartialMessage, *, fail_if_not_exists: bool = Tru channel_id=message.channel.id, guild_id=getattr(message.guild, 'id', None), fail_if_not_exists=fail_if_not_exists, + type=type, ) self._state = message._state return self @@ -567,7 +722,9 @@ def __repr__(self) -> str: return f'' def to_dict(self) -> MessageReferencePayload: - result: Dict[str, Any] = {'message_id': self.message_id} if self.message_id is not None else {} + result: Dict[str, Any] = ( + {'type': self.type.value, 'message_id': self.message_id} if self.message_id is not None else {} + ) result['channel_id'] = self.channel_id if self.guild_id is not None: result['guild_id'] = self.guild_id @@ -1699,7 +1856,12 @@ async def end_poll(self) -> Message: return Message(state=self._state, channel=self.channel, data=data) - def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: + def to_reference( + self, + *, + fail_if_not_exists: bool = True, + type: MessageReferenceType = MessageReferenceType.reply, + ) -> MessageReference: """Creates a :class:`~discord.MessageReference` from the current message. .. versionadded:: 1.6 @@ -1711,6 +1873,10 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 + type: :class:`MessageReferenceType` + The type of message reference. + + .. versionadded:: 2.5 Returns --------- @@ -1718,7 +1884,44 @@ def to_reference(self, *, fail_if_not_exists: bool = True) -> MessageReference: The reference to this message. """ - return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists) + return MessageReference.from_message(self, fail_if_not_exists=fail_if_not_exists, type=type) + + async def forward( + self, + destination: MessageableChannel, + *, + fail_if_not_exists: bool = True, + ) -> Message: + """|coro| + + Forwards this message to a channel. + + .. versionadded:: 2.5 + + Parameters + ---------- + destination: :class:`~discord.abc.Messageable` + The channel to forward this message to. + fail_if_not_exists: :class:`bool` + Whether replying using the message reference should raise :class:`HTTPException` + if the message no longer exists or Discord could not fetch the message. + + Raises + ------ + ~discord.HTTPException + Forwarding the message failed. + + Returns + ------- + :class:`.Message` + The message sent to the channel. + """ + reference = self.to_reference( + fail_if_not_exists=fail_if_not_exists, + type=MessageReferenceType.forward, + ) + ret = await destination.send(reference=reference) + return ret def to_message_reference_dict(self) -> MessageReferencePayload: data: MessageReferencePayload = { @@ -1881,6 +2084,10 @@ class Message(PartialMessage, Hashable): purchase_notification: Optional[:class:`PurchaseNotification`] The data of the purchase notification that prompted this :attr:`MessageType.purchase_notification` message. + .. versionadded:: 2.5 + message_snapshots: List[:class:`MessageSnapshot`] + The message snapshots attached to this message. + .. versionadded:: 2.5 """ @@ -1920,6 +2127,7 @@ class Message(PartialMessage, Hashable): 'poll', 'call', 'purchase_notification', + 'message_snapshots', ) if TYPE_CHECKING: @@ -1958,6 +2166,7 @@ def __init__( self.position: Optional[int] = data.get('position') self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] + self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots')) self.poll: Optional[Poll] = None try: diff --git a/discord/types/message.py b/discord/types/message.py index c1972541a254..24f6065d158b 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -102,7 +102,11 @@ class MessageApplication(TypedDict): cover_image: NotRequired[str] +MessageReferenceType = Literal[0, 1] + + class MessageReference(TypedDict, total=False): + type: MessageReferenceType message_id: Snowflake channel_id: Required[Snowflake] guild_id: Snowflake @@ -173,6 +177,20 @@ class CallMessage(TypedDict): ] +class MessageSnapshot(TypedDict): + type: MessageType + content: str + embeds: List[Embed] + attachments: List[Attachment] + timestamp: str + edited_timestamp: Optional[str] + flags: NotRequired[int] + mentions: List[UserWithMember] + mention_roles: SnowflakeList + stickers_items: NotRequired[List[StickerItem]] + components: NotRequired[List[Component]] + + class Message(PartialMessage): id: Snowflake author: User diff --git a/docs/api.rst b/docs/api.rst index 6f728d56b3c6..338368910145 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3810,6 +3810,24 @@ of :class:`enum.Enum`. The subscription is inactive and not being charged. +.. class:: MessageReferenceType + + Represents the type of a message reference. + + .. versionadded:: 2.5 + + .. attribute:: reply + + A message reply. + + .. attribute:: forward + + A forwarded message. + + .. attribute:: default + + An alias for :attr:`.reply`. + .. _discord-api-audit-logs: Audit Log Data @@ -5353,6 +5371,14 @@ PollAnswer .. _discord_api_data: +MessageSnapshot +~~~~~~~~~~~~~~~~~ + +.. attributetable:: MessageSnapshot + +.. autoclass:: MessageSnapshot + :members: + Data Classes -------------- From b207c8a1ac3e53184c4930b4d3077617d29653f0 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:01:54 +0200 Subject: [PATCH 131/354] [commands] Add perms object param to default_permissions decorator Closes #9951 --- discord/app_commands/commands.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index cd6eafaf3de9..a872fb4be276 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2821,7 +2821,7 @@ def inner(f: T) -> T: return inner -def default_permissions(**perms: bool) -> Callable[[T], T]: +def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: bool) -> Callable[[T], T]: r"""A decorator that sets the default permissions needed to execute this command. When this decorator is used, by default users must have these permissions to execute the command. @@ -2845,8 +2845,12 @@ def default_permissions(**perms: bool) -> Callable[[T], T]: ----------- \*\*perms: :class:`bool` Keyword arguments denoting the permissions to set as the default. + perms_obj: :class:`~discord.Permissions` + A permissions object as positional argument. This can be used in combination with ``**perms``. - Example + .. versionadded:: 2.5 + + Examples --------- .. code-block:: python3 @@ -2855,9 +2859,21 @@ def default_permissions(**perms: bool) -> Callable[[T], T]: @app_commands.default_permissions(manage_messages=True) async def test(interaction: discord.Interaction): await interaction.response.send_message('You may or may not have manage messages.') + + .. code-block:: python3 + + ADMIN_PERMS = discord.Permissions(administrator=True) + + @app_commands.command() + @app_commands.default_permissions(ADMIN_PERMS, manage_messages=True) + async def test(interaction: discord.Interaction): + await interaction.response.send_message('You may or may not have manage messages.') """ - permissions = Permissions(**perms) + if perms_obj is not None: + permissions = perms_obj | Permissions(**perms) + else: + permissions = Permissions(**perms) def decorator(func: T) -> T: if isinstance(func, (Command, Group, ContextMenu)): From 20875646a3581c2712843103ef4935bea899f8b4 Mon Sep 17 00:00:00 2001 From: z03h <7235242+z03h@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:48:22 -0700 Subject: [PATCH 132/354] Fix MessageReferenceType not being public --- discord/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/enums.py b/discord/enums.py index 5a361b2259cf..4fe5f3ffae1e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -76,6 +76,7 @@ 'PollLayoutType', 'VoiceChannelEffectAnimationType', 'SubscriptionStatus', + 'MessageReferenceType', ) From 5734996aaf4495a7fb05b7ab272a18e4d451cd22 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 12 Oct 2024 18:51:21 -0400 Subject: [PATCH 133/354] Fix soundboard sounds event data type Closes #9969 --- discord/state.py | 23 ++++++++++++----------- discord/types/gateway.py | 6 +++++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/discord/state.py b/discord/state.py index 83628af3243f..df6073985d7c 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1609,18 +1609,19 @@ def parse_guild_soundboard_sound_delete(self, data: gw.GuildSoundBoardSoundDelet _log.debug('GUILD_SOUNDBOARD_SOUND_DELETE referencing unknown guild ID: %s. Discarding.', guild_id) def parse_guild_soundboard_sounds_update(self, data: gw.GuildSoundBoardSoundsUpdateEvent) -> None: - for raw_sound in data: - guild_id = int(raw_sound['guild_id']) # type: ignore # can't be None here - guild = self._get_guild(guild_id) - if guild is not None: - sound_id = int(raw_sound['sound_id']) - sound = guild.get_soundboard_sound(sound_id) - if sound is not None: - self._update_and_dispatch_sound_update(sound, raw_sound) - else: - _log.warning('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) + guild_id = int(data['guild_id']) + guild = self._get_guild(guild_id) + if guild is None: + _log.debug('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + return + + for raw_sound in data['soundboard_sounds']: + sound_id = int(raw_sound['sound_id']) + sound = guild.get_soundboard_sound(sound_id) + if sound is not None: + self._update_and_dispatch_sound_update(sound, raw_sound) else: - _log.debug('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown guild ID: %s. Discarding.', guild_id) + _log.warning('GUILD_SOUNDBOARD_SOUNDS_UPDATE referencing unknown sound ID: %s. Discarding.', sound_id) def parse_application_command_permissions_update(self, data: GuildApplicationCommandPermissionsPayload): raw = RawAppCommandPermissionsUpdateEvent(data=data, state=self) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 6261c70dd864..5b35b5360c99 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -324,7 +324,11 @@ class _GuildScheduledEventUsersEvent(TypedDict): VoiceChannelEffectSendEvent = VoiceChannelEffect GuildSoundBoardSoundCreateEvent = GuildSoundBoardSoundUpdateEvent = SoundboardSound -GuildSoundBoardSoundsUpdateEvent = List[SoundboardSound] + + +class GuildSoundBoardSoundsUpdateEvent(TypedDict): + guild_id: Snowflake + soundboard_sounds: List[SoundboardSound] class GuildSoundBoardSoundDeleteEvent(TypedDict): From 442ad40ab23201f96c37c77e15bbacaf9dd385c1 Mon Sep 17 00:00:00 2001 From: z03h <7235242+z03h@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:49:50 -0700 Subject: [PATCH 134/354] [commands] Add SoundboardSoundConverter --- discord/ext/commands/converter.py | 40 +++++++++++++++++++++++++++++++ discord/ext/commands/errors.py | 19 +++++++++++++++ docs/ext/commands/api.rst | 4 ++++ 3 files changed, 63 insertions(+) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 6c559009dbae..744a00fd3020 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -82,6 +82,7 @@ 'GuildChannelConverter', 'GuildStickerConverter', 'ScheduledEventConverter', + 'SoundboardSoundConverter', 'clean_content', 'Greedy', 'Range', @@ -951,6 +952,44 @@ async def convert(self, ctx: Context[BotT], argument: str) -> discord.ScheduledE return result +class SoundboardSoundConverter(IDConverter[discord.SoundboardSound]): + """Converts to a :class:`~discord.SoundboardSound`. + + Lookups are done for the local guild if available. Otherwise, for a DM context, + lookup is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by name. + + .. versionadded:: 2.5 + """ + + async def convert(self, ctx: Context[BotT], argument: str) -> discord.SoundboardSound: + guild = ctx.guild + match = self._get_id_match(argument) + result = None + + if match: + # ID match + sound_id = int(match.group(1)) + if guild: + result = guild.get_soundboard_sound(sound_id) + else: + result = ctx.bot.get_soundboard_sound(sound_id) + else: + # lookup by name + if guild: + result = discord.utils.get(guild.soundboard_sounds, name=argument) + else: + result = discord.utils.get(ctx.bot.soundboard_sounds, name=argument) + if result is None: + raise SoundboardSoundNotFound(argument) + + return result + + class clean_content(Converter[str]): """Converts the argument to mention scrubbed version of said content. @@ -1263,6 +1302,7 @@ def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: discord.GuildSticker: GuildStickerConverter, discord.ScheduledEvent: ScheduledEventConverter, discord.ForumChannel: ForumChannelConverter, + discord.SoundboardSound: SoundboardSoundConverter, } diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 0c1e0f2d0db4..0c3cfa0c4933 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -75,6 +75,7 @@ 'EmojiNotFound', 'GuildStickerNotFound', 'ScheduledEventNotFound', + 'SoundboardSoundNotFound', 'PartialEmojiConversionFailure', 'BadBoolArgument', 'MissingRole', @@ -564,6 +565,24 @@ def __init__(self, argument: str) -> None: super().__init__(f'ScheduledEvent "{argument}" not found.') +class SoundboardSoundNotFound(BadArgument): + """Exception raised when the bot can not find the soundboard sound. + + This inherits from :exc:`BadArgument` + + .. versionadded:: 2.5 + + Attributes + ----------- + argument: :class:`str` + The sound supplied by the caller that was not found + """ + + def __init__(self, argument: str) -> None: + self.argument: str = argument + super().__init__(f'SoundboardSound "{argument}" not found.') + + class BadBoolArgument(BadArgument): """Exception raised when a boolean argument was not convertable. diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index 9bda24f6e890..f55225614215 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -708,6 +708,9 @@ Exceptions .. autoexception:: discord.ext.commands.ScheduledEventNotFound :members: +.. autoexception:: discord.ext.commands.SoundboardSoundNotFound + :members: + .. autoexception:: discord.ext.commands.BadBoolArgument :members: @@ -800,6 +803,7 @@ Exception Hierarchy - :exc:`~.commands.EmojiNotFound` - :exc:`~.commands.GuildStickerNotFound` - :exc:`~.commands.ScheduledEventNotFound` + - :exc:`~.commands.SoundboardSoundNotFound` - :exc:`~.commands.PartialEmojiConversionFailure` - :exc:`~.commands.BadBoolArgument` - :exc:`~.commands.RangeError` From ca85782b351a210b13eafc0f502e18b13a973f0e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 13 Oct 2024 14:26:40 -0400 Subject: [PATCH 135/354] [commands] Fix Context.defer unconditionally deferring --- discord/ext/commands/context.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index ad9c286eef57..5a74fa5f3a9e 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -87,11 +87,15 @@ def __init__(self, ctx: Context[BotT], *, ephemeral: bool): self.ctx: Context[BotT] = ctx self.ephemeral: bool = ephemeral + async def do_defer(self) -> None: + if self.ctx.interaction and not self.ctx.interaction.response.is_done(): + await self.ctx.interaction.response.defer(ephemeral=self.ephemeral) + def __await__(self) -> Generator[Any, None, None]: - return self.ctx.defer(ephemeral=self.ephemeral).__await__() + return self.do_defer().__await__() async def __aenter__(self) -> None: - await self.ctx.defer(ephemeral=self.ephemeral) + await self.do_defer() async def __aexit__( self, From 9da131ed268701dd5e6017723ca8d8b379c353d3 Mon Sep 17 00:00:00 2001 From: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com> Date: Mon, 14 Oct 2024 18:58:24 -0400 Subject: [PATCH 136/354] Fix variance typing issue with CommandTree.error decorator --- discord/app_commands/tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index bc0d68ec7938..90b9a21ab958 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -73,7 +73,7 @@ from .commands import ContextMenuCallback, CommandCallback, P, T ErrorFunc = Callable[ - [Interaction, AppCommandError], + [Interaction[ClientT], AppCommandError], Coroutine[Any, Any, Any], ] @@ -833,7 +833,7 @@ async def on_error(self, interaction: Interaction[ClientT], error: AppCommandErr else: _log.error('Ignoring exception in command tree', exc_info=error) - def error(self, coro: ErrorFunc) -> ErrorFunc: + def error(self, coro: ErrorFunc[ClientT]) -> ErrorFunc[ClientT]: """A decorator that registers a coroutine as a local error handler. This must match the signature of the :meth:`on_error` callback. From b0c66b7734a5bde4f739105bdf16041c32ab43e4 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:43:35 +0200 Subject: [PATCH 137/354] Fix NameError in sku.py --- discord/sku.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/sku.py b/discord/sku.py index 9ad325366da4..f3e457505354 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -27,6 +27,8 @@ from typing import AsyncIterator, Optional, TYPE_CHECKING +from datetime import datetime + from . import utils from .enums import try_enum, SKUType, EntitlementType from .flags import SKUFlags @@ -34,8 +36,6 @@ from .subscription import Subscription if TYPE_CHECKING: - from datetime import datetime - from .abc import SnowflakeTime, Snowflake from .guild import Guild from .state import ConnectionState From c5e74068f0d0d84a27c557018aa15b3f2132e5c7 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:48:49 +0200 Subject: [PATCH 138/354] [commands] Unwrap Parameter if given as default to commands.parameter --- discord/ext/commands/parameters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py index 70df39534aef..33592c74a6b5 100644 --- a/discord/ext/commands/parameters.py +++ b/discord/ext/commands/parameters.py @@ -247,6 +247,12 @@ async def wave(ctx, to: discord.User = commands.parameter(default=lambda ctx: ct .. versionadded:: 2.3 """ + if isinstance(default, Parameter): + if displayed_default is empty: + displayed_default = default._displayed_default + + default = default._default + return Parameter( name='empty', kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, From 354ae4208c860f68d0404f8b86b53af7724411ff Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:49:41 +0200 Subject: [PATCH 139/354] Fix abc.GuildChannel.clone implementations --- discord/abc.py | 6 ++---- discord/channel.py | 43 +++++++++++++------------------------------ 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 57c26ad90f86..bbf22c201810 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1039,10 +1039,6 @@ async def clone( .. versionadded:: 1.1 - .. versionchanged:: 2.5 - - The ``category`` keyword-only parameter was added. - Parameters ------------ name: Optional[:class:`str`] @@ -1051,6 +1047,8 @@ async def clone( category: Optional[:class:`~discord.CategoryChannel`] The category the new channel belongs to. This parameter is ignored if cloning a category channel. + + .. versionadded:: 2.5 reason: Optional[:class:`str`] The reason for cloning this channel. Shows up on the audit log. diff --git a/discord/channel.py b/discord/channel.py index b8858f356693..58fc2524f40b 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -532,14 +532,16 @@ async def clone( category: Optional[CategoryChannel] = None, reason: Optional[str] = None, ) -> TextChannel: + base: Dict[Any, Any] = { + 'topic': self.topic, + 'nsfw': self.nsfw, + 'default_auto_archive_duration': self.default_auto_archive_duration, + 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, + } + if not self.is_news(): + base['rate_limit_per_user'] = self.slowmode_delay return await self._clone_impl( - { - 'topic': self.topic, - 'rate_limit_per_user': self.slowmode_delay, - 'nsfw': self.nsfw, - 'default_auto_archive_duration': self.default_auto_archive_duration, - 'default_thread_rate_limit_per_user': self.default_thread_slowmode_delay, - }, + base, name=name, category=category, reason=reason, @@ -1395,7 +1397,9 @@ async def create_webhook(self, *, name: str, avatar: Optional[bytes] = None, rea return Webhook.from_state(data, state=self._state) @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> Self: + async def clone( + self, *, name: Optional[str] = None, category: Optional[CategoryChannel] = None, reason: Optional[str] = None + ) -> Self: base = { 'bitrate': self.bitrate, 'user_limit': self.user_limit, @@ -1409,6 +1413,7 @@ async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = Non return await self._clone_impl( base, name=name, + category=category, reason=reason, ) @@ -1506,18 +1511,6 @@ def type(self) -> Literal[ChannelType.voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone( - self, - *, - name: Optional[str] = None, - category: Optional[CategoryChannel] = None, - reason: Optional[str] = None, - ) -> VoiceChannel: - return await self._clone_impl( - {'bitrate': self.bitrate, 'user_limit': self.user_limit}, name=name, category=category, reason=reason - ) - @overload async def edit(self) -> None: ... @@ -1788,16 +1781,6 @@ def type(self) -> Literal[ChannelType.stage_voice]: """:class:`ChannelType`: The channel's Discord type.""" return ChannelType.stage_voice - @utils.copy_doc(discord.abc.GuildChannel.clone) - async def clone( - self, - *, - name: Optional[str] = None, - category: Optional[CategoryChannel] = None, - reason: Optional[str] = None, - ) -> StageChannel: - return await self._clone_impl({}, name=name, category=category, reason=reason) - @property def instance(self) -> Optional[StageInstance]: """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. From c8ecbd8d100b0e285987e33ef27209fce8bc71d1 Mon Sep 17 00:00:00 2001 From: Steve C Date: Tue, 22 Oct 2024 12:01:34 -0400 Subject: [PATCH 140/354] Add Message.forward flag --- discord/abc.py | 9 +++++---- discord/flags.py | 10 +++++++++- discord/message.py | 8 +++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index bbf22c201810..af2b15dac79c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1530,10 +1530,11 @@ async def send( .. versionadded:: 1.4 reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`, :class:`~discord.PartialMessage`] - A reference to the :class:`~discord.Message` to which you are replying, this can be created using - :meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. You can control - whether this mentions the author of the referenced message using the :attr:`~discord.AllowedMentions.replied_user` - attribute of ``allowed_mentions`` or by setting ``mention_author``. + A reference to the :class:`~discord.Message` to which you are referencing, this can be created using + :meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. + In the event of a replying reference, you can control whether this mentions the author of the referenced + message using the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions`` or by + setting ``mention_author``. .. versionadded:: 1.6 diff --git a/discord/flags.py b/discord/flags.py index abe77f3c2670..de806ba9c046 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -135,7 +135,7 @@ def __init__(self, **kwargs: bool): setattr(self, key, value) @classmethod - def _from_value(cls, value): + def _from_value(cls, value: int) -> Self: self = cls.__new__(cls) self.value = value return self @@ -490,6 +490,14 @@ def voice(self): """ return 8192 + @flag_value + def forwarded(self): + """:class:`bool`: Returns ``True`` if the message is a forwarded message. + + .. versionadded:: 2.5 + """ + return 16384 + @fill_with_flags() class PublicUserFlags(BaseFlags): diff --git a/discord/message.py b/discord/message.py index 44025f460fee..1f2cea3ae9ba 100644 --- a/discord/message.py +++ b/discord/message.py @@ -615,7 +615,7 @@ class MessageReference: guild_id: Optional[:class:`int`] The guild id of the message referenced. fail_if_not_exists: :class:`bool` - Whether replying to the referenced message should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 @@ -627,8 +627,6 @@ class MessageReference: If the message was resolved at a prior point but has since been deleted then this will be of type :class:`DeletedReferencedMessage`. - Currently, this is mainly the replied to message when a user replies to a message. - .. versionadded:: 1.6 """ @@ -680,7 +678,7 @@ def from_message( message: :class:`~discord.Message` The message to be converted into a reference. fail_if_not_exists: :class:`bool` - Whether replying to the referenced message should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 @@ -1869,7 +1867,7 @@ def to_reference( Parameters ---------- fail_if_not_exists: :class:`bool` - Whether replying using the message reference should raise :class:`HTTPException` + Whether the referenced message should raise :class:`HTTPException` if the message no longer exists or Discord could not fetch the message. .. versionadded:: 1.7 From e94fb455274d78969922082bb9822f88345984d5 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 22 Oct 2024 19:26:46 -0400 Subject: [PATCH 141/354] Add note about using venvs on Linux --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 621a69500291..b2112f9d6b81 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,13 @@ Installing To install the library without full voice support, you can just run the following command: +.. note:: + + A `Virtual Environment `__ is recommended to install + the library, especially on Linux where the system Python is externally managed and restricts which + packages you can install on it. + + .. code:: sh # Linux/macOS From a0b0a97e5256721e8b994ecafc6a721a8a7f80d0 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 27 Oct 2024 14:49:41 -0400 Subject: [PATCH 142/354] 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 143/354] 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 144/354] 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 145/354] 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 146/354] [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 147/354] 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 148/354] 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 149/354] 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 150/354] 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: From 7f9535704137b16c663c17e8c1865375cfd226f7 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:10:19 +0100 Subject: [PATCH 151/354] Add mention property to PartialMessageable --- discord/channel.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/channel.py b/discord/channel.py index 02dd9024beb1..f7ba9c25f49d 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -3529,6 +3529,14 @@ def permissions_for(self, obj: Any = None, /) -> Permissions: return Permissions.none() + @property + def mention(self) -> str: + """:class:`str`: Returns a string that allows you to mention the channel. + + .. versionadded:: 2.5 + """ + return f'<#{self.id}>' + def get_partial_message(self, message_id: int, /) -> PartialMessage: """Creates a :class:`PartialMessage` from the message ID. From 9806aeb83179d0d1e90d903e30db7e69e0d492e5 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 1 Dec 2024 16:19:09 -0500 Subject: [PATCH 152/354] Add public method to get session start limits --- discord/http.py | 5 +++-- discord/shard.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++- docs/api.rst | 10 ++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/discord/http.py b/discord/http.py index ac1254e3f052..7c3f198c98c1 100644 --- a/discord/http.py +++ b/discord/http.py @@ -97,6 +97,7 @@ subscription, ) from .types.snowflake import Snowflake, SnowflakeList + from .types.gateway import SessionStartLimit from types import TracebackType @@ -2753,13 +2754,13 @@ def get_sku_subscription(self, sku_id: Snowflake, subscription_id: Snowflake) -> # Misc - async def get_bot_gateway(self) -> Tuple[int, str]: + async def get_bot_gateway(self) -> Tuple[int, str, SessionStartLimit]: try: data = await self.request(Route('GET', '/gateway/bot')) except HTTPException as exc: raise GatewayNotFound() from exc - return data['shards'], data['url'] + return data['shards'], data['url'], data['session_start_limit'] def get_user(self, user_id: Snowflake) -> Response[user.User]: return self.request(Route('GET', '/users/{user_id}', user_id=user_id)) diff --git a/discord/shard.py b/discord/shard.py index 52155aa24dfe..eeb240c95a6f 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -47,13 +47,16 @@ from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, Optional, List, Dict if TYPE_CHECKING: + from typing_extensions import Unpack from .gateway import DiscordWebSocket from .activity import BaseActivity from .flags import Intents + from .types.gateway import SessionStartLimit __all__ = ( 'AutoShardedClient', 'ShardInfo', + 'SessionStartLimits', ) _log = logging.getLogger(__name__) @@ -293,6 +296,32 @@ def is_ws_ratelimited(self) -> bool: return self._parent.ws.is_ratelimited() +class SessionStartLimits: + """A class that holds info about session start limits + + Attributes + ---------- + total: :class:`int` + The total number of session starts the current user is allowed + remaining: :class:`int` + Remaining remaining number of session starts the current user is allowed + reset_after: :class:`int` + The number of milliseconds until the limit resets + max_concurrency: :class:`int` + The number of identify requests allowed per 5 seconds + + .. versionadded:: 2.5 + """ + + __slots__ = ("total", "remaining", "reset_after", "max_concurrency") + + def __init__(self, **kwargs: Unpack[SessionStartLimit]): + self.total: int = kwargs['total'] + self.remaining: int = kwargs['remaining'] + self.reset_after: int = kwargs['reset_after'] + self.max_concurrency: int = kwargs['max_concurrency'] + + class AutoShardedClient(Client): """A client similar to :class:`Client` except it handles the complications of sharding for the user into a more manageable and transparent single @@ -415,6 +444,33 @@ def shards(self) -> Dict[int, ShardInfo]: """Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object.""" return {shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items()} + async def fetch_session_start_limits(self) -> SessionStartLimits: + """|coro| + + Get the session start limits. + + This is not typically needed, and will be handled for you by default. + + At the point where you are launching multiple instances + with manual shard ranges and are considered required to use large bot + sharding by Discord, this function when used along IPC and a + before_identity_hook can speed up session start. + + .. versionadded:: 2.5 + + Returns + ------- + :class:`SessionStartLimits` + A class containing the session start limits + + Raises + ------ + GatewayNotFound + The gateway was unreachable + """ + _, _, limits = await self.http.get_bot_gateway() + return SessionStartLimits(**limits) + async def launch_shard(self, gateway: yarl.URL, shard_id: int, *, initial: bool = False) -> None: try: coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id) @@ -434,7 +490,7 @@ async def launch_shards(self) -> None: if self.shard_count is None: self.shard_count: int - self.shard_count, gateway_url = await self.http.get_bot_gateway() + self.shard_count, gateway_url, _session_start_limit = await self.http.get_bot_gateway() gateway = yarl.URL(gateway_url) else: gateway = DiscordWebSocket.DEFAULT_GATEWAY diff --git a/docs/api.rst b/docs/api.rst index 338368910145..ca0fb4ef4645 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1381,7 +1381,7 @@ Subscriptions .. function:: on_subscription_delete(subscription) Called when a subscription is deleted. - + .. versionadded:: 2.5 :param subscription: The subscription that was deleted. @@ -5209,6 +5209,14 @@ ShardInfo .. autoclass:: ShardInfo() :members: +SessionStartLimits +~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SessionStartLimits + +.. autoclass:: SessionStartLimits() + :members: + SKU ~~~~~~~~~~~ From fcd9239b33d18596fc90e166393fffd98a9487c4 Mon Sep 17 00:00:00 2001 From: Ginger <75683114+gingershaped@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:01:47 -0500 Subject: [PATCH 153/354] Add Client.fetch_guild_preview method --- discord/client.py | 25 +++++++++- discord/guild.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++ discord/http.py | 3 ++ docs/api.rst | 7 +++ 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index ff02bf7b6f00..8ecff6ec23d4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -53,7 +53,7 @@ from .invite import Invite from .template import Template from .widget import Widget -from .guild import Guild +from .guild import Guild, GuildPreview from .emoji import Emoji from .channel import _threaded_channel_factory, PartialMessageable from .enums import ChannelType, EntitlementOwnerType @@ -2356,6 +2356,29 @@ async def fetch_guild(self, guild_id: int, /, *, with_counts: bool = True) -> Gu data = await self.http.get_guild(guild_id, with_counts=with_counts) return Guild(data=data, state=self._connection) + async def fetch_guild_preview(self, guild_id: int) -> GuildPreview: + """|coro| + + Retrieves a preview of a :class:`.Guild` from an ID. If the guild is discoverable, + you don't have to be a member of it. + + .. versionadded:: 2.5 + + Raises + ------ + NotFound + The guild doesn't exist, or is not discoverable and you are not in it. + HTTPException + Getting the guild failed. + + Returns + -------- + :class:`.GuildPreview` + The guild preview from the ID. + """ + data = await self.http.get_guild_preview(guild_id) + return GuildPreview(data=data, state=self._connection) + async def create_guild( self, *, diff --git a/discord/guild.py b/discord/guild.py index fc39179abeb2..faf64e27923c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -99,6 +99,7 @@ __all__ = ( 'Guild', + 'GuildPreview', 'BanEntry', ) @@ -109,6 +110,7 @@ from .types.guild import ( Ban as BanPayload, Guild as GuildPayload, + GuildPreview as GuildPreviewPayload, RolePositionUpdate as RolePositionUpdatePayload, GuildFeature, IncidentData, @@ -160,6 +162,121 @@ class _GuildLimit(NamedTuple): filesize: int +class GuildPreview(Hashable): + """Represents a preview of a Discord guild. + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two guild previews are equal. + + .. describe:: x != y + + Checks if two guild previews are not equal. + + .. describe:: hash(x) + + Returns the guild's hash. + + .. describe:: str(x) + + Returns the guild's name. + + Attributes + ---------- + name: :class:`str` + The guild preview's name. + id: :class:`int` + The guild preview's ID. + features: List[:class:`str`] + A list of features the guild has. See :attr:`Guild.features` for more information. + description: Optional[:class:`str`] + The guild preview's description. + emojis: Tuple[:class:`Emoji`, ...] + All emojis that the guild owns. + stickers: Tuple[:class:`GuildSticker`, ...] + All stickers that the guild owns. + approximate_member_count: :class:`int` + The approximate number of members in the guild. + approximate_presence_count: :class:`int` + The approximate number of members currently active in in the guild. Offline members are excluded. + """ + + __slots__ = ( + '_state', + '_icon', + '_splash', + '_discovery_splash', + 'id', + 'name', + 'emojis', + 'stickers', + 'features', + 'description', + "approximate_member_count", + "approximate_presence_count", + ) + + def __init__(self, *, data: GuildPreviewPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.id = int(data['id']) + self.name: str = data['name'] + self._icon: Optional[str] = data.get('icon') + self._splash: Optional[str] = data.get('splash') + self._discovery_splash: Optional[str] = data.get('discovery_splash') + self.emojis: Tuple[Emoji, ...] = tuple( + map( + lambda d: Emoji(guild=state._get_or_create_unavailable_guild(self.id), state=state, data=d), + data.get('emojis', []), + ) + ) + self.stickers: Tuple[GuildSticker, ...] = tuple( + map(lambda d: GuildSticker(state=state, data=d), data.get('stickers', [])) + ) + self.features: List[GuildFeature] = data.get('features', []) + self.description: Optional[str] = data.get('description') + self.approximate_member_count: int = data.get('approximate_member_count') + self.approximate_presence_count: int = data.get('approximate_presence_count') + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return ( + f'<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r} ' + f'features={self.features}>' + ) + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the guild's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def icon(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + if self._icon is None: + return None + return Asset._from_guild_icon(self._state, self.id, self._icon) + + @property + def splash(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + if self._splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes') + + @property + def discovery_splash(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available.""" + if self._discovery_splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._discovery_splash, path='discovery-splashes') + + class Guild(Hashable): """Represents a Discord guild. diff --git a/discord/http.py b/discord/http.py index 7c3f198c98c1..5cb6cb58cb01 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1453,6 +1453,9 @@ def get_guild(self, guild_id: Snowflake, *, with_counts: bool = True) -> Respons params = {'with_counts': int(with_counts)} return self.request(Route('GET', '/guilds/{guild_id}', guild_id=guild_id), params=params) + def get_guild_preview(self, guild_id: Snowflake) -> Response[guild.GuildPreview]: + return self.request(Route('GET', '/guilds/{guild_id}/preview', guild_id=guild_id)) + def delete_guild(self, guild_id: Snowflake) -> Response[None]: return self.request(Route('DELETE', '/guilds/{guild_id}', guild_id=guild_id)) diff --git a/docs/api.rst b/docs/api.rst index ca0fb4ef4645..0b4015f78175 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4815,6 +4815,13 @@ Guild :type: List[:class:`Object`] +GuildPreview +~~~~~~~~~~~~ + +.. attributetable:: GuildPreview + +.. autoclass:: GuildPreview + :members: ScheduledEvent ~~~~~~~~~~~~~~ From bb5a4703a703767ffb34131c23e49080c626c190 Mon Sep 17 00:00:00 2001 From: Lev Bernstein <10897595+LevBernstein@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:17:18 -0500 Subject: [PATCH 154/354] Bump Sphinx to 5.3.0 --- docs/conf.py | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 28b39452cfba..74c51fd71bf6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -374,6 +374,10 @@ def _i18n_warning_filter(record: logging.LogRecord) -> bool: # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False +# If true, create table of contents entries for domain objects (e.g. functions, +# classes, attributes, etc.). +toc_object_entries=False + def setup(app): if app.config.language == 'ja': app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None) diff --git a/pyproject.toml b/pyproject.toml index 4ec7bc007de7..627fd72a76f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = { file = "requirements.txt" } [project.optional-dependencies] voice = ["PyNaCl>=1.3.0,<1.6"] docs = [ - "sphinx==4.4.0", + "sphinx==5.3.0", "sphinxcontrib_trio==1.1.2", # TODO: bump these when migrating to a newer Sphinx version "sphinxcontrib-websupport==1.2.4", From f2aa0b833c05bb60bd1c308e6d604c383fbf85f8 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 15 Jan 2025 18:18:02 -0500 Subject: [PATCH 155/354] [tasks] Fix race condition with set_result --- discord/ext/tasks/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 81644da3aa44..57f9e741bc6b 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -111,12 +111,17 @@ def __init__(self, dt: datetime.datetime, *, loop: asyncio.AbstractEventLoop) -> self.loop: asyncio.AbstractEventLoop = loop self.future: asyncio.Future[None] = loop.create_future() relative_delta = discord.utils.compute_timedelta(dt) - self.handle = loop.call_later(relative_delta, self.future.set_result, None) + self.handle = loop.call_later(relative_delta, self._wrapped_set_result, self.future) + + @staticmethod + def _wrapped_set_result(future: asyncio.Future) -> None: + if not future.done(): + future.set_result(None) def recalculate(self, dt: datetime.datetime) -> None: self.handle.cancel() relative_delta = discord.utils.compute_timedelta(dt) - self.handle: asyncio.TimerHandle = self.loop.call_later(relative_delta, self.future.set_result, None) + self.handle: asyncio.TimerHandle = self.loop.call_later(relative_delta, self._wrapped_set_result, self.future) def wait(self) -> asyncio.Future[Any]: return self.future From eb15aa8ca4639a99ee7a76c632f228db9b30c528 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:19:49 +0100 Subject: [PATCH 156/354] Add exclude_deleted parameter to Client.entitlements --- discord/client.py | 6 ++++++ discord/http.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/discord/client.py b/discord/client.py index 8ecff6ec23d4..f33253bc9cdb 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2824,6 +2824,7 @@ async def entitlements( user: Optional[Snowflake] = None, guild: Optional[Snowflake] = None, exclude_ended: bool = False, + exclude_deleted: bool = True, ) -> AsyncIterator[Entitlement]: """Retrieves an :term:`asynchronous iterator` of the :class:`.Entitlement` that applications has. @@ -2865,6 +2866,10 @@ async def entitlements( The guild to filter by. exclude_ended: :class:`bool` Whether to exclude ended entitlements. Defaults to ``False``. + exclude_deleted: :class:`bool` + Whether to exclude deleted entitlements. Defaults to ``True``. + + .. versionadded:: 2.5 Raises ------- @@ -2901,6 +2906,7 @@ async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Op user_id=user.id if user else None, guild_id=guild.id if guild else None, exclude_ended=exclude_ended, + exclude_deleted=exclude_deleted, ) if data: diff --git a/discord/http.py b/discord/http.py index 5cb6cb58cb01..fd0acae3713d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2460,6 +2460,7 @@ def get_entitlements( limit: Optional[int] = None, guild_id: Optional[Snowflake] = None, exclude_ended: Optional[bool] = None, + exclude_deleted: Optional[bool] = None, ) -> Response[List[sku.Entitlement]]: params: Dict[str, Any] = {} @@ -2477,6 +2478,8 @@ def get_entitlements( params['guild_id'] = guild_id if exclude_ended is not None: params['exclude_ended'] = int(exclude_ended) + if exclude_deleted is not None: + params['exclude_deleted'] = int(exclude_deleted) return self.request( Route('GET', '/applications/{application_id}/entitlements', application_id=application_id), params=params From 6214942f860db2b3a41ff3b8aada5eef881d91e8 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:22:04 +0100 Subject: [PATCH 157/354] Fix SessionStartLimits and SKU.subscriptions docstrings --- discord/shard.py | 4 ++-- discord/sku.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/shard.py b/discord/shard.py index eeb240c95a6f..454fd5e2895a 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -299,6 +299,8 @@ def is_ws_ratelimited(self) -> bool: class SessionStartLimits: """A class that holds info about session start limits + .. versionadded:: 2.5 + Attributes ---------- total: :class:`int` @@ -309,8 +311,6 @@ class SessionStartLimits: The number of milliseconds until the limit resets max_concurrency: :class:`int` The number of identify requests allowed per 5 seconds - - .. versionadded:: 2.5 """ __slots__ = ("total", "remaining", "reset_after", "max_concurrency") diff --git a/discord/sku.py b/discord/sku.py index f3e457505354..46bdf94bfaf5 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -146,12 +146,12 @@ async def subscriptions( Usage :: - async for subscription in sku.subscriptions(limit=100): + async for subscription in sku.subscriptions(limit=100, user=user): print(subscription.user_id, subscription.current_period_end) Flattening into a list :: - subscriptions = [subscription async for subscription in sku.subscriptions(limit=100)] + subscriptions = [subscription async for subscription in sku.subscriptions(limit=100, user=user)] # subscriptions is now a list of Subscription... All parameters are optional. From 9dc8e2712aa9376b6563b457223fd2c4bfb16f9e Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Thu, 16 Jan 2025 00:22:28 +0100 Subject: [PATCH 158/354] Add ForumChannel.members property --- discord/channel.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/channel.py b/discord/channel.py index f7ba9c25f49d..a306707d6fdb 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2492,6 +2492,14 @@ def type(self) -> Literal[ChannelType.forum, ChannelType.media]: def _sorting_bucket(self) -> int: return ChannelType.text.value + @property + def members(self) -> List[Member]: + """List[:class:`Member`]: Returns all members that can see this channel. + + .. versionadded:: 2.5 + """ + return [m for m in self.guild.members if self.permissions_for(m).read_messages] + @property def _scheduled_event_entity_type(self) -> Optional[EntityType]: return None From ed95f2f106ad9cdb045ed0aee4b7110ee7204567 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:25:57 +0100 Subject: [PATCH 159/354] Parse full message for raw message edit event --- discord/raw_models.py | 18 ++++++++++-------- discord/state.py | 18 +++++++++++------- discord/types/gateway.py | 3 +-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 8d3ad328fb4c..012b8f07da34 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -166,20 +166,22 @@ class RawMessageUpdateEvent(_RawReprMixin): cached_message: Optional[:class:`Message`] The cached message, if found in the internal message cache. Represents the message before it is modified by the data in :attr:`RawMessageUpdateEvent.data`. + message: :class:`Message` + The updated message. + + .. versionadded:: 2.5 """ - __slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message') + __slots__ = ('message_id', 'channel_id', 'guild_id', 'data', 'cached_message', 'message') - def __init__(self, data: MessageUpdateEvent) -> None: - self.message_id: int = int(data['id']) - self.channel_id: int = int(data['channel_id']) + def __init__(self, data: MessageUpdateEvent, message: Message) -> None: + self.message_id: int = message.id + self.channel_id: int = message.channel.id self.data: MessageUpdateEvent = data + self.message: Message = message self.cached_message: Optional[Message] = None - try: - self.guild_id: Optional[int] = int(data['guild_id']) - except KeyError: - self.guild_id: Optional[int] = None + self.guild_id: Optional[int] = message.guild.id if message.guild else None class RawReactionActionEvent(_RawReprMixin): diff --git a/discord/state.py b/discord/state.py index df6073985d7c..8dad83a88f66 100644 --- a/discord/state.py +++ b/discord/state.py @@ -690,17 +690,21 @@ def parse_message_delete_bulk(self, data: gw.MessageDeleteBulkEvent) -> None: self._messages.remove(msg) # type: ignore def parse_message_update(self, data: gw.MessageUpdateEvent) -> None: - raw = RawMessageUpdateEvent(data) - message = self._get_message(raw.message_id) - if message is not None: - older_message = copy.copy(message) + channel, _ = self._get_guild_channel(data) + # channel would be the correct type here + updated_message = Message(channel=channel, data=data, state=self) # type: ignore + + raw = RawMessageUpdateEvent(data=data, message=updated_message) + cached_message = self._get_message(updated_message.id) + if cached_message is not None: + older_message = copy.copy(cached_message) raw.cached_message = older_message self.dispatch('raw_message_edit', raw) - message._update(data) + cached_message._update(data) # Coerce the `after` parameter to take the new updated Member # ref: #5999 - older_message.author = message.author - self.dispatch('message_edit', older_message, message) + older_message.author = updated_message.author + self.dispatch('message_edit', older_message, updated_message) else: self.dispatch('raw_message_edit', raw) diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 5b35b5360c99..7dca5badc356 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -92,8 +92,7 @@ class MessageDeleteBulkEvent(TypedDict): guild_id: NotRequired[Snowflake] -class MessageUpdateEvent(Message): - channel_id: Snowflake +MessageUpdateEvent = MessageCreateEvent class MessageReactionAddEvent(TypedDict): From 7c8503fefbf5d75734e528b5525c647df6302b8e Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:27:03 +0100 Subject: [PATCH 160/354] Fix callable FlagConverter defaults on hybrid commands fix: Callable FlagConverter defaults being returned as-is on interaction based calls. --- discord/ext/commands/hybrid.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index e84e7e03e9e5..af9e63a7b383 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -234,6 +234,12 @@ def replace_parameter( descriptions[name] = flag.description if flag.name != flag.attribute: renames[name] = flag.name + if pseudo.default is not pseudo.empty: + # This ensures the default is wrapped around _CallableDefault if callable + # else leaves it as-is. + pseudo = pseudo.replace( + default=_CallableDefault(flag.default) if callable(flag.default) else flag.default + ) mapping[name] = pseudo @@ -283,7 +289,7 @@ def replace_parameters( param = param.replace(default=default) if isinstance(param.default, Parameter): - # If we're here, then then it hasn't been handled yet so it should be removed completely + # If we're here, then it hasn't been handled yet so it should be removed completely param = param.replace(default=parameter.empty) # Flags are flattened out and thus don't get their parameter in the actual mapping From 1646471ab84ee656346109dfaabc6f9ed80c8956 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 15 Jan 2025 18:33:12 -0500 Subject: [PATCH 161/354] Revert "Bump Sphinx to 5.3.0" This reverts commit bb5a4703a703767ffb34131c23e49080c626c190. --- docs/conf.py | 4 ---- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 74c51fd71bf6..28b39452cfba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -374,10 +374,6 @@ def _i18n_warning_filter(record: logging.LogRecord) -> bool: # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False -# If true, create table of contents entries for domain objects (e.g. functions, -# classes, attributes, etc.). -toc_object_entries=False - def setup(app): if app.config.language == 'ja': app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None) diff --git a/pyproject.toml b/pyproject.toml index 627fd72a76f1..4ec7bc007de7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = { file = "requirements.txt" } [project.optional-dependencies] voice = ["PyNaCl>=1.3.0,<1.6"] docs = [ - "sphinx==5.3.0", + "sphinx==4.4.0", "sphinxcontrib_trio==1.1.2", # TODO: bump these when migrating to a newer Sphinx version "sphinxcontrib-websupport==1.2.4", From 55974ebde866619eeff02093c325b8556addf1fa Mon Sep 17 00:00:00 2001 From: Violet <110789901+vionya@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:40:16 -0600 Subject: [PATCH 162/354] Fix MessageSnapshot sticker_items typo --- discord/message.py | 2 +- discord/types/message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index a58921531159..3d755e314d7c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -528,7 +528,7 @@ def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): self.created_at: datetime.datetime = utils.parse_time(data['timestamp']) self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) - self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('stickers_items', [])] + self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] self.components: List[MessageComponentType] = [] for component_data in data.get('components', []): diff --git a/discord/types/message.py b/discord/types/message.py index 24f6065d158b..1ec86681b66e 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -187,7 +187,7 @@ class MessageSnapshot(TypedDict): flags: NotRequired[int] mentions: List[UserWithMember] mention_roles: SnowflakeList - stickers_items: NotRequired[List[StickerItem]] + sticker_items: NotRequired[List[StickerItem]] components: NotRequired[List[Component]] From cf6d7ff47aaa1adecf3de3e9eab56626dff1acb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 15 Jan 2025 23:50:40 +0000 Subject: [PATCH 163/354] Fix bug due to typo in async pagination of entitlements loop --- discord/client.py | 2 +- discord/sku.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index f33253bc9cdb..c7410011e3f8 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2955,7 +2955,7 @@ async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Opti data, state, limit = await strategy(retrieve, state, limit) # Terminate loop on next iteration; there's no data left after this - if len(data) < 1000: + if len(data) < 100: limit = 0 for e in data: diff --git a/discord/sku.py b/discord/sku.py index 46bdf94bfaf5..3516370b4ee1 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -239,7 +239,7 @@ async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Opti data, state, limit = await strategy(retrieve, state, limit) # Terminate loop on next iteration; there's no data left after this - if len(data) < 1000: + if len(data) < 100: limit = 0 for e in data: From 1537102402e7ac5445a5d9f91677e9f352a8587f Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 15 Jan 2025 18:50:59 -0500 Subject: [PATCH 164/354] Temporary dependency for docs extra on python 3.13+ --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4ec7bc007de7..bda91c6b486e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ docs = [ "sphinxcontrib-serializinghtml==1.1.5", "typing-extensions>=4.3,<5", "sphinx-inline-tabs==2023.4.21", + # TODO: Remove this when moving to Sphinx >= 6.6 + "imghdr-lts==1.0.0; python_version>='3.13'", ] speed = [ "orjson>=3.5.4", From fa1cc00a29122e498309d9d29704397802664917 Mon Sep 17 00:00:00 2001 From: Nathan Waxman-Jeng <54194229+ilovetocode2019@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:57:46 -0800 Subject: [PATCH 165/354] Return new instance in Poll.end to avoid inconsistencies --- discord/poll.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/poll.py b/discord/poll.py index 88ed5b534d2f..720f91245aab 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -359,6 +359,7 @@ def _update(self, message: Message) -> None: # The message's poll contains the more up to date data. self._expiry = message.poll.expires_at self._finalized = message.poll._finalized + self._answers = message.poll._answers def _update_results(self, data: PollResultPayload) -> None: self._finalized = data['is_finalized'] @@ -568,6 +569,7 @@ async def end(self) -> Self: if not self._message or not self._state: # Make type checker happy raise ClientException('This poll has no attached message.') - self._message = await self._message.end_poll() + message = await self._message.end_poll() + self._update(message) return self From 1edec93ed33e4bf129ada9fcbbd36d19cc86febd Mon Sep 17 00:00:00 2001 From: scruz Date: Thu, 16 Jan 2025 05:29:09 +0530 Subject: [PATCH 166/354] Update Member.timed_out_until docstring --- discord/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/member.py b/discord/member.py index 388e854835e5..6118e3267eca 100644 --- a/discord/member.py +++ b/discord/member.py @@ -303,7 +303,7 @@ class Member(discord.abc.Messageable, _UserTag): "Nitro boost" on the guild, if available. This could be ``None``. timed_out_until: Optional[:class:`datetime.datetime`] An aware datetime object that specifies the date and time in UTC that the member's time out will expire. - This will be set to ``None`` if the user is not timed out. + This will be set to ``None`` or a time in the past if the user is not timed out. .. versionadded:: 2.0 """ From 2c3938dd5135f9fdc5cc3978db14e882c6b2d6b5 Mon Sep 17 00:00:00 2001 From: tom <92334622+tom-jm69@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:01:09 +0100 Subject: [PATCH 167/354] Update Client.create_application_emoji docs --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index c7410011e3f8..ef7980ec4b9f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -3200,7 +3200,7 @@ async def create_application_emoji( Parameters ---------- name: :class:`str` - The emoji name. Must be at least 2 characters. + The emoji name. Must be between 2 and 32 characters long. image: :class:`bytes` The :term:`py:bytes-like object` representing the image data to use. Only JPG, PNG and GIF images are supported. From 7db391189db0e74d5b640b319b14c3eb00f1f0f2 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 16 Jan 2025 05:31:42 +0530 Subject: [PATCH 168/354] Add __repr__ to Interaction --- discord/interactions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/interactions.py b/discord/interactions.py index e0fb7ed8654b..49bfbfb07203 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -192,6 +192,9 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState[ClientT]) self.command_failed: bool = False self._from_data(data) + def __repr__(self) -> str: + return f'<{self.__class__.__name__} id={self.id} type={self.type!r} guild_id={self.guild_id!r} user={self.user!r}>' + def _from_data(self, data: InteractionPayload): self.id: int = int(data['id']) self.type: InteractionType = try_enum(InteractionType, data['type']) From d95605839ff052ae2af0d52622bb340dd97ae8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Thu, 16 Jan 2025 00:02:39 +0000 Subject: [PATCH 169/354] [commands] Fix _fallback attr not being set on replace for Parameter --- discord/ext/commands/parameters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py index 33592c74a6b5..196530d94c8b 100644 --- a/discord/ext/commands/parameters.py +++ b/discord/ext/commands/parameters.py @@ -135,7 +135,7 @@ def replace( if displayed_name is MISSING: displayed_name = self._displayed_name - return self.__class__( + ret = self.__class__( name=name, kind=kind, default=default, @@ -144,6 +144,8 @@ def replace( displayed_default=displayed_default, displayed_name=displayed_name, ) + ret._fallback = self._fallback + return ret if not TYPE_CHECKING: # this is to prevent anything breaking if inspect internals change name = _gen_property('name') From d5c80b6cf591b4dd7034297ab5e99a824af2c302 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:43:22 +0100 Subject: [PATCH 170/354] Add optional dev dependencies --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bda91c6b486e..d7360731df31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,10 @@ test = [ "typing-extensions>=4.3,<5", "tzdata; sys_platform == 'win32'", ] +dev = [ + "black==22.6", + "typing_extensions>=4.3,<5", +] [tool.setuptools] packages = [ From b1b736971a3326135fdf18480424d282aead81ca Mon Sep 17 00:00:00 2001 From: Dep <70801324+Depreca1ed@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:43:59 +0530 Subject: [PATCH 171/354] Added scopeless kwarg to discord.utils.oauth_url --- discord/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/utils.py b/discord/utils.py index 905735cfb406..9b6bd59a2ced 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -327,7 +327,7 @@ def oauth_url( permissions: Permissions = MISSING, guild: Snowflake = MISSING, redirect_uri: str = MISSING, - scopes: Iterable[str] = MISSING, + scopes: Optional[Iterable[str]] = MISSING, disable_guild_select: bool = False, state: str = MISSING, ) -> str: @@ -369,7 +369,8 @@ def oauth_url( The OAuth2 URL for inviting the bot into guilds. """ url = f'https://discord.com/oauth2/authorize?client_id={client_id}' - url += '&scope=' + '+'.join(scopes or ('bot', 'applications.commands')) + if scopes is not None: + url += '&scope=' + '+'.join(scopes or ('bot', 'applications.commands')) if permissions is not MISSING: url += f'&permissions={permissions.value}' if guild is not MISSING: From 743ef27dd7eb58464fe05bf2657c7694982d7f7b Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Sun, 19 Jan 2025 05:31:02 +0100 Subject: [PATCH 172/354] [commands] Correct ExtensionNotFound error message Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/ext/commands/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 0c3cfa0c4933..f81d54b4d89c 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -1080,7 +1080,7 @@ class ExtensionNotFound(ExtensionError): """ def __init__(self, name: str) -> None: - msg = f'Extension {name!r} could not be loaded.' + msg = f'Extension {name!r} could not be loaded or found.' super().__init__(msg, name=name) From ff2ad34be58e67888a43f76d49fc32d6751a27eb Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 19 Jan 2025 10:22:03 +0100 Subject: [PATCH 173/354] Add support for multiple subscription tier SKUs --- discord/subscription.py | 4 ++++ discord/types/subscription.py | 1 + 2 files changed, 5 insertions(+) diff --git a/discord/subscription.py b/discord/subscription.py index d861615abca3..ec6d7c3e5ba5 100644 --- a/discord/subscription.py +++ b/discord/subscription.py @@ -63,6 +63,8 @@ class Subscription(Hashable): canceled_at: Optional[:class:`datetime.datetime`] When the subscription was canceled. This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`. + renewal_sku_ids: List[:class:`int`] + The IDs of the SKUs that the user is going to be subscribed to when renewing. """ __slots__ = ( @@ -75,6 +77,7 @@ class Subscription(Hashable): 'current_period_end', 'status', 'canceled_at', + 'renewal_sku_ids', ) def __init__(self, *, state: ConnectionState, data: SubscriptionPayload): @@ -88,6 +91,7 @@ def __init__(self, *, state: ConnectionState, data: SubscriptionPayload): self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end']) self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status']) self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at']) + self.renewal_sku_ids: List[int] = list(map(int, data['renewal_sku_ids'] or [])) def __repr__(self) -> str: return f'' diff --git a/discord/types/subscription.py b/discord/types/subscription.py index bb707afce15f..8d4c020703c3 100644 --- a/discord/types/subscription.py +++ b/discord/types/subscription.py @@ -40,3 +40,4 @@ class Subscription(TypedDict): current_period_end: str status: SubscriptionStatus canceled_at: Optional[str] + renewal_sku_ids: Optional[List[Snowflake]] From afbbc07e980cabd7ce52e3d4c67f16758e705a3e Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:09:05 +0100 Subject: [PATCH 174/354] Add support for poll result messages --- discord/enums.py | 1 + discord/message.py | 16 +++++++ discord/poll.py | 96 ++++++++++++++++++++++++++++++++++++++-- discord/state.py | 21 +++++++++ discord/types/embed.py | 2 +- discord/types/message.py | 1 + docs/api.rst | 4 ++ 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 4fe5f3ffae1e..ce772cc87285 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -266,6 +266,7 @@ class MessageType(Enum): guild_incident_report_raid = 38 guild_incident_report_false_alarm = 39 purchase_notification = 44 + poll_result = 46 class SpeakingState(Enum): diff --git a/discord/message.py b/discord/message.py index 3d755e314d7c..3016d2f2945c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2268,6 +2268,13 @@ def __init__( # the channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + if self.type is MessageType.poll_result: + if isinstance(self.reference.resolved, self.__class__): + self._state._update_poll_results(self, self.reference.resolved) + else: + if self.reference.message_id: + self._state._update_poll_results(self, self.reference.message_id) + self.application: Optional[MessageApplication] = None try: application = data['application'] @@ -2634,6 +2641,7 @@ def is_system(self) -> bool: MessageType.chat_input_command, MessageType.context_menu_command, MessageType.thread_starter_message, + MessageType.poll_result, ) @utils.cached_slot_property('_cs_system_content') @@ -2810,6 +2818,14 @@ def system_content(self) -> str: if guild_product_purchase is not None: return f'{self.author.name} has purchased {guild_product_purchase.product_name}!' + if self.type is MessageType.poll_result: + embed = self.embeds[0] # Will always have 1 embed + poll_title = utils.get( + embed.fields, + name='poll_question_text', + ) + return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore + # Fallback for unknown message types return '' diff --git a/discord/poll.py b/discord/poll.py index 720f91245aab..767f8ffae82a 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -29,7 +29,7 @@ import datetime -from .enums import PollLayoutType, try_enum +from .enums import PollLayoutType, try_enum, MessageType from . import utils from .emoji import PartialEmoji, Emoji from .user import User @@ -125,7 +125,16 @@ class PollAnswer: Whether the current user has voted to this answer or not. """ - __slots__ = ('media', 'id', '_state', '_message', '_vote_count', 'self_voted', '_poll') + __slots__ = ( + 'media', + 'id', + '_state', + '_message', + '_vote_count', + 'self_voted', + '_poll', + '_victor', + ) def __init__( self, @@ -141,6 +150,7 @@ def __init__( self._vote_count: int = 0 self.self_voted: bool = False self._poll: Poll = poll + self._victor: bool = False def _handle_vote_event(self, added: bool, self_voted: bool) -> None: if added: @@ -210,6 +220,19 @@ def _to_dict(self) -> PollAnswerPayload: 'poll_media': self.media.to_dict(), } + @property + def victor(self) -> bool: + """:class:`bool`: Whether the answer is the one that had the most + votes when the poll ended. + + .. versionadded:: 2.5 + + .. note:: + + If the poll has not ended, this will always return ``False``. + """ + return self._victor + async def voters( self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None ) -> AsyncIterator[Union[User, Member]]: @@ -325,6 +348,8 @@ class Poll: '_expiry', '_finalized', '_state', + '_total_votes', + '_victor_answer_id', ) def __init__( @@ -348,6 +373,8 @@ def __init__( self._state: Optional[ConnectionState] = None self._finalized: bool = False self._expiry: Optional[datetime.datetime] = None + self._total_votes: Optional[int] = None + self._victor_answer_id: Optional[int] = None def _update(self, message: Message) -> None: self._state = message._state @@ -360,6 +387,33 @@ def _update(self, message: Message) -> None: self._expiry = message.poll.expires_at self._finalized = message.poll._finalized self._answers = message.poll._answers + self._update_results_from_message(message) + + def _update_results_from_message(self, message: Message) -> None: + if message.type != MessageType.poll_result or not message.embeds: + return + + result_embed = message.embeds[0] # Will always have 1 embed + fields: Dict[str, str] = {field.name: field.value for field in result_embed.fields} # type: ignore + + total_votes = fields.get('total_votes') + + if total_votes is not None: + self._total_votes = int(total_votes) + + victor_answer = fields.get('victor_answer_id') + + if victor_answer is None: + return # Can't do anything else without the victor answer + + self._victor_answer_id = int(victor_answer) + + victor_answer_votes = fields['victor_answer_votes'] + + answer = self._answers[self._victor_answer_id] + answer._victor = True + answer._vote_count = int(victor_answer_votes) + self._answers[answer.id] = answer # Ensure update def _update_results(self, data: PollResultPayload) -> None: self._finalized = data['is_finalized'] @@ -432,6 +486,32 @@ def answers(self) -> List[PollAnswer]: """List[:class:`PollAnswer`]: Returns a read-only copy of the answers.""" return list(self._answers.values()) + @property + def victor_answer_id(self) -> Optional[int]: + """Optional[:class:`int`]: The victor answer ID. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + return self._victor_answer_id + + @property + def victor_answer(self) -> Optional[PollAnswer]: + """Optional[:class:`PollAnswer`]: The victor answer. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + if self.victor_answer_id is None: + return None + return self.get_answer(self.victor_answer_id) + @property def expires_at(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry. @@ -457,12 +537,20 @@ def created_at(self) -> Optional[datetime.datetime]: @property def message(self) -> Optional[Message]: - """:class:`Message`: The message this poll is from.""" + """Optional[:class:`Message`]: The message this poll is from.""" return self._message @property def total_votes(self) -> int: - """:class:`int`: Returns the sum of all the answer votes.""" + """:class:`int`: Returns the sum of all the answer votes. + + If the poll has not yet finished, this is an approximate vote count. + + .. versionchanged:: 2.5 + This now returns an exact vote count when updated from its poll results message. + """ + if self._total_votes is not None: + return self._total_votes return sum([answer.vote_count for answer in self.answers]) def is_finalised(self) -> bool: diff --git a/discord/state.py b/discord/state.py index 8dad83a88f66..453fbc5b6474 100644 --- a/discord/state.py +++ b/discord/state.py @@ -552,6 +552,27 @@ def _update_poll_counts(self, message: Message, answer_id: int, added: bool, sel poll._handle_vote(answer_id, added, self_voted) return poll + def _update_poll_results(self, from_: Message, to: Union[Message, int]) -> None: + if isinstance(to, Message): + cached = self._get_message(to.id) + elif isinstance(to, int): + cached = self._get_message(to) + + if cached is None: + return + + to = cached + else: + return + + if to.poll is None: + return + + to.poll._update_results_from_message(from_) + + if cached is not None and cached.poll: + cached.poll._update_results_from_message(from_) + async def chunker( self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None ) -> None: diff --git a/discord/types/embed.py b/discord/types/embed.py index f2f1c5a9f1e1..376df3a1a7d0 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -71,7 +71,7 @@ class EmbedAuthor(TypedDict, total=False): proxy_icon_url: str -EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link'] +EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link', 'poll_result'] class Embed(TypedDict, total=False): diff --git a/discord/types/message.py b/discord/types/message.py index 1ec86681b66e..ae38db46f8c0 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -174,6 +174,7 @@ class CallMessage(TypedDict): 38, 39, 44, + 46, ] diff --git a/docs/api.rst b/docs/api.rst index 0b4015f78175..b9348ec4b033 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1887,6 +1887,10 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: poll_result + + The system message sent when a poll has closed. + .. class:: UserFlags Represents Discord User flags. From 418a7915e687090179a67d12de807db0db4f284c Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:49:50 +1000 Subject: [PATCH 175/354] Add raw presence update evemt --- discord/__init__.py | 1 + discord/client.py | 9 +++ discord/guild.py | 9 +-- discord/member.py | 88 ++++++++----------------- discord/presences.py | 150 ++++++++++++++++++++++++++++++++++++++++++ discord/raw_models.py | 12 +--- discord/state.py | 31 +++++---- discord/utils.py | 8 +++ docs/api.rst | 39 +++++++++++ setup.py | 1 + 10 files changed, 262 insertions(+), 86 deletions(-) create mode 100644 discord/presences.py diff --git a/discord/__init__.py b/discord/__init__.py index c206f650f66f..f850ee4acbea 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -72,6 +72,7 @@ from .poll import * from .soundboard import * from .subscription import * +from .presences import * class VersionInfo(NamedTuple): diff --git a/discord/client.py b/discord/client.py index ef7980ec4b9f..83296148c69d 100644 --- a/discord/client.py +++ b/discord/client.py @@ -237,6 +237,15 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 + enable_raw_presences: :class:`bool` + Whether to manually enable or disable the :func:`on_raw_presence_update` event. + + Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. + + By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` + is disabled, otherwise it's set to ``False``. + + .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``. This allows you to check requests the library is using. For more information, check the diff --git a/discord/guild.py b/discord/guild.py index faf64e27923c..b7e53f0c79ba 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,7 +95,7 @@ from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound - +from .presences import RawPresenceUpdateEvent __all__ = ( 'Guild', @@ -653,10 +653,11 @@ def _from_data(self, guild: GuildPayload) -> None: empty_tuple = () for presence in guild.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state) + member = self.get_member(raw_presence.user_id) + if member is not None: - member._presence_update(presence, empty_tuple) # type: ignore + member._presence_update(raw_presence, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] diff --git a/discord/member.py b/discord/member.py index 6118e3267eca..2de8fbfc1f34 100644 --- a/discord/member.py +++ b/discord/member.py @@ -36,13 +36,13 @@ from .asset import Asset from .utils import MISSING from .user import BaseUser, ClientUser, User, _UserTag -from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import Status, try_enum +from .enums import Status from .errors import ClientException from .colour import Colour from .object import Object from .flags import MemberFlags +from .presences import ClientStatus __all__ = ( 'VoiceState', @@ -57,10 +57,8 @@ from .channel import DMChannel, VoiceChannel, StageChannel from .flags import PublicUserFlags from .guild import Guild - from .types.activity import ( - ClientStatus as ClientStatusPayload, - PartialPresenceUpdate, - ) + from .activity import ActivityTypes + from .presences import RawPresenceUpdateEvent from .types.member import ( MemberWithUser as MemberWithUserPayload, Member as MemberPayload, @@ -168,46 +166,6 @@ def __repr__(self) -> str: return f'<{self.__class__.__name__} {inner}>' -class _ClientStatus: - __slots__ = ('_status', 'desktop', 'mobile', 'web') - - def __init__(self): - self._status: str = 'offline' - - self.desktop: Optional[str] = None - self.mobile: Optional[str] = None - self.web: Optional[str] = None - - def __repr__(self) -> str: - attrs = [ - ('_status', self._status), - ('desktop', self.desktop), - ('mobile', self.mobile), - ('web', self.web), - ] - inner = ' '.join('%s=%r' % t for t in attrs) - return f'<{self.__class__.__name__} {inner}>' - - def _update(self, status: str, data: ClientStatusPayload, /) -> None: - self._status = status - - self.desktop = data.get('desktop') - self.mobile = data.get('mobile') - self.web = data.get('web') - - @classmethod - def _copy(cls, client_status: Self, /) -> Self: - self = cls.__new__(cls) # bypass __init__ - - self._status = client_status._status - - self.desktop = client_status.desktop - self.mobile = client_status.mobile - self.web = client_status.web - - return self - - def flatten_user(cls: T) -> T: for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): # ignore private/special methods @@ -306,6 +264,10 @@ class Member(discord.abc.Messageable, _UserTag): This will be set to ``None`` or a time in the past if the user is not timed out. .. versionadded:: 2.0 + client_status: :class:`ClientStatus` + Model which holds information about the status of the member on various clients/platforms via presence updates. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -318,7 +280,7 @@ class Member(discord.abc.Messageable, _UserTag): 'nick', 'timed_out_until', '_permissions', - '_client_status', + 'client_status', '_user', '_state', '_avatar', @@ -354,7 +316,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at')) self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since')) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles'])) - self._client_status: _ClientStatus = _ClientStatus() + self.client_status: ClientStatus = ClientStatus() self.activities: Tuple[ActivityTypes, ...] = () self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) @@ -430,7 +392,7 @@ def _copy(cls, member: Self) -> Self: self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self._client_status = _ClientStatus._copy(member._client_status) + self.client_status = member.client_status self.guild = member.guild self.nick = member.nick self.pending = member.pending @@ -473,13 +435,12 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') - def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: - self.activities = tuple(create_activity(d, self._state) for d in data['activities']) - self._client_status._update(data['status'], data['client_status']) + def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]: + self.activities = raw.activities + self.client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) - return None def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user @@ -518,7 +479,7 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: @property def status(self) -> Status: """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return try_enum(Status, self._client_status._status) + return self.client_status.status @property def raw_status(self) -> str: @@ -526,31 +487,36 @@ def raw_status(self) -> str: .. versionadded:: 1.5 """ - return self._client_status._status + return self.client_status._status @status.setter def status(self, value: Status) -> None: # internal use only - self._client_status._status = str(value) + self.client_status._status = str(value) @property def mobile_status(self) -> Status: """:class:`Status`: The member's status on a mobile device, if applicable.""" - return try_enum(Status, self._client_status.mobile or 'offline') + return self.client_status.mobile_status @property def desktop_status(self) -> Status: """:class:`Status`: The member's status on the desktop client, if applicable.""" - return try_enum(Status, self._client_status.desktop or 'offline') + return self.client_status.desktop_status @property def web_status(self) -> Status: """:class:`Status`: The member's status on the web client, if applicable.""" - return try_enum(Status, self._client_status.web or 'offline') + return self.client_status.web_status def is_on_mobile(self) -> bool: - """:class:`bool`: A helper function that determines if a member is active on a mobile device.""" - return self._client_status.mobile is not None + """A helper function that determines if a member is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self.client_status.is_on_mobile() @property def colour(self) -> Colour: diff --git a/discord/presences.py b/discord/presences.py new file mode 100644 index 000000000000..7fec2a09dfcc --- /dev/null +++ b/discord/presences.py @@ -0,0 +1,150 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Tuple + +from .activity import create_activity +from .enums import Status, try_enum +from .utils import MISSING, _get_as_snowflake, _RawReprMixin + +if TYPE_CHECKING: + from typing_extensions import Self + + from .activity import ActivityTypes + from .guild import Guild + from .state import ConnectionState + from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate + + +__all__ = ( + 'RawPresenceUpdateEvent', + 'ClientStatus', +) + + +class ClientStatus: + """Represents the :ddocs:`Client Status Object ` from Discord, + which holds information about the status of the user on various clients/platforms, with additional helpers. + + .. versionadded:: 2.5 + """ + + __slots__ = ('_status', 'desktop', 'mobile', 'web') + + def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None: + self._status: str = status or 'offline' + + data = data or {} + self.desktop: Optional[str] = data.get('desktop') + self.mobile: Optional[str] = data.get('mobile') + self.web: Optional[str] = data.get('web') + + def __repr__(self) -> str: + attrs = [ + ('_status', self._status), + ('desktop', self.desktop), + ('mobile', self.mobile), + ('web', self.web), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {inner}>' + + def _update(self, status: str, data: ClientStatusPayload, /) -> None: + self._status = status + + self.desktop = data.get('desktop') + self.mobile = data.get('mobile') + self.web = data.get('web') + + @classmethod + def _copy(cls, client_status: Self, /) -> Self: + self = cls.__new__(cls) # bypass __init__ + + self._status = client_status._status + + self.desktop = client_status.desktop + self.mobile = client_status.mobile + self.web = client_status.web + + return self + + @property + def status(self) -> Status: + """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" + return try_enum(Status, self._status) + + @property + def raw_status(self) -> str: + """:class:`str`: The user's overall status as a string value.""" + return self._status + + @property + def mobile_status(self) -> Status: + """:class:`Status`: The user's status on a mobile device, if applicable.""" + return try_enum(Status, self.mobile or 'offline') + + @property + def desktop_status(self) -> Status: + """:class:`Status`: The user's status on the desktop client, if applicable.""" + return try_enum(Status, self.desktop or 'offline') + + @property + def web_status(self) -> Status: + """:class:`Status`: The user's status on the web client, if applicable.""" + return try_enum(Status, self.web or 'offline') + + def is_on_mobile(self) -> bool: + """:class:`bool`: A helper function that determines if a user is active on a mobile device.""" + return self.mobile is not None + + +class RawPresenceUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_presence_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that triggered the presence update. + guild_id: Optional[:class:`int`] + The guild ID for the users presence update. Could be ``None``. + guild: Optional[:class:`Guild`] + The guild associated with the presence update and user. Could be ``None``. + client_status: :class:`ClientStatus` + The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. + activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] + The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information. + """ + + __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities') + + def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self.user_id: int = int(data['user']['id']) + self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status']) + self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities']) + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.guild: Optional[Guild] = state._get_guild(self.guild_id) diff --git a/discord/raw_models.py b/discord/raw_models.py index 012b8f07da34..c8c8b0e388ef 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -25,10 +25,10 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union +from typing import TYPE_CHECKING, Literal, Optional, Set, List, Union from .enums import ChannelType, try_enum, ReactionType -from .utils import _get_as_snowflake +from .utils import _get_as_snowflake, _RawReprMixin from .app_commands import AppCommandPermissions from .colour import Colour @@ -82,14 +82,6 @@ ) -class _RawReprMixin: - __slots__: Tuple[str, ...] = () - - def __repr__(self) -> str: - value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) - return f'<{self.__class__.__name__} {value}>' - - class RawMessageDeleteEvent(_RawReprMixin): """Represents the event payload for a :func:`on_raw_message_delete` event. diff --git a/discord/state.py b/discord/state.py index 453fbc5b6474..b1409f809100 100644 --- a/discord/state.py +++ b/discord/state.py @@ -62,6 +62,7 @@ from .channel import * from .channel import _channel_factory from .raw_models import * +from .presences import RawPresenceUpdateEvent from .member import Member from .role import Role from .enums import ChannelType, try_enum, Status @@ -261,6 +262,10 @@ def __init__( if not intents.members or cache_flags._empty: self.store_user = self.store_user_no_intents + self.raw_presence_flag: bool = options.get('enable_raw_presences', utils.MISSING) + if self.raw_presence_flag is utils.MISSING: + self.raw_presence_flag = not intents.members and intents.presences + self.parsers: Dict[str, Callable[[Any], None]] self.parsers = parsers = {} for attr, func in inspect.getmembers(self): @@ -827,22 +832,24 @@ def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None: self.dispatch('interaction', interaction) def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: - guild_id = utils._get_as_snowflake(data, 'guild_id') - # guild_id won't be None here - guild = self._get_guild(guild_id) - if guild is None: - _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) + raw = RawPresenceUpdateEvent(data=data, state=self) + + if self.raw_presence_flag: + self.dispatch('raw_presence_update', raw) + + if raw.guild is None: + _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) return - user = data['user'] - member_id = int(user['id']) - member = guild.get_member(member_id) + member = raw.guild.get_member(raw.user_id) + if member is None: - _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id) + _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', raw.user_id) return old_member = Member._copy(member) - user_update = member._presence_update(data=data, user=user) + user_update = member._presence_update(raw=raw, user=data['user']) + if user_update: self.dispatch('user_update', user_update[0], user_update[1]) @@ -1430,8 +1437,10 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) + if member is not None: - member._presence_update(presence, user) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self) + member._presence_update(raw_presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) diff --git a/discord/utils.py b/discord/utils.py index 9b6bd59a2ced..bd327b5a8bb1 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1532,3 +1532,11 @@ def _format_call_duration(duration: datetime.timedelta) -> str: formatted = f"{years} years" return formatted + + +class _RawReprMixin: + __slots__: Tuple[str, ...] = () + + def __repr__(self) -> str: + value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) + return f'<{self.__class__.__name__} {value}>' diff --git a/docs/api.rst b/docs/api.rst index b9348ec4b033..8da2ba80c894 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -916,6 +916,29 @@ Members :param after: The updated member's updated info. :type after: :class:`Member` +.. function:: on_raw_presence_update(payload) + + Called when a :class:`Member` updates their presence. + + This requires :attr:`Intents.presences` to be enabled. + + Unlike :func:`on_presence_update`, when enabled, this is called regardless of the state of internal guild + and member caches, and **does not** provide a comparison between the previous and updated states of the :class:`Member`. + + .. important:: + + By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` + is disabled. + + You can manually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, + however :attr:`Intents.presences` is always required for this event to work. + + .. versionadded:: 2.5 + + :param payload: The raw presence update event model. + :type payload: :class:`RawPresenceUpdateEvent` + + Messages ~~~~~~~~~ @@ -5364,6 +5387,14 @@ RawPollVoteActionEvent .. autoclass:: RawPollVoteActionEvent() :members: +RawPresenceUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPresenceUpdateEvent + +.. autoclass:: RawPresenceUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ @@ -5398,6 +5429,14 @@ MessageSnapshot .. autoclass:: MessageSnapshot :members: +ClientStatus +~~~~~~~~~~~~ + +.. attributetable:: ClientStatus + +.. autoclass:: ClientStatus() + :members: + Data Classes -------------- diff --git a/setup.py b/setup.py index 2481afeb428c..e3d6d59f4fff 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup import re + def derive_version() -> str: version = '' with open('discord/__init__.py') as f: From db7b2d9058e4e7f357a6a9f02aad41e8b053a1bd Mon Sep 17 00:00:00 2001 From: Michael H Date: Tue, 21 Jan 2025 21:45:56 -0500 Subject: [PATCH 176/354] Change default file size limit Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/utils.py b/discord/utils.py index bd327b5a8bb1..1caffe7f57f0 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -108,7 +108,7 @@ ) DISCORD_EPOCH = 1420070400000 -DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400 +DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760 class _MissingSentinel: From 88d7bd127587246829dbd8cfbb76bfef30ff5d2e Mon Sep 17 00:00:00 2001 From: iyad-f <128908811+iyad-f@users.noreply.github.com> Date: Sun, 9 Feb 2025 07:17:41 +0530 Subject: [PATCH 177/354] Fix message_pin, message_unpin target id being None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael H Co-authored-by: Alex Nørgaard --- discord/audit_logs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 59d563829257..af67855d4584 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -874,7 +874,13 @@ def _convert_target_invite(self, target_id: None) -> Invite: def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]: return self._state.get_emoji(target_id) or Object(id=target_id, type=Emoji) - def _convert_target_message(self, target_id: int) -> Union[Member, User, Object]: + def _convert_target_message(self, target_id: Optional[int]) -> Optional[Union[Member, User, Object]]: + # The message_pin and message_unpin action types do not have a + # non-null target_id so safeguard against that + + if target_id is None: + return None + return self._get_member(target_id) or Object(id=target_id, type=Member) def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]: From 7f511360b8862a9b819c780485698b490b66ddf5 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Sun, 9 Feb 2025 03:00:11 +0100 Subject: [PATCH 178/354] Fix wait_for overloads --- discord/client.py | 254 +++++++++++++++++++++++----------------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/discord/client.py b/discord/client.py index 83296148c69d..b997bd96f4af 100644 --- a/discord/client.py +++ b/discord/client.py @@ -1213,8 +1213,8 @@ async def wait_for( event: Literal['raw_app_command_permissions_update'], /, *, - check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawAppCommandPermissionsUpdateEvent: ... @@ -1224,8 +1224,8 @@ async def wait_for( event: Literal['app_command_completion'], /, *, - check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]: ... @@ -1237,8 +1237,8 @@ async def wait_for( event: Literal['automod_rule_create', 'automod_rule_update', 'automod_rule_delete'], /, *, - check: Optional[Callable[[AutoModRule], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModRule], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModRule: ... @@ -1248,8 +1248,8 @@ async def wait_for( event: Literal['automod_action'], /, *, - check: Optional[Callable[[AutoModAction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModAction], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModAction: ... @@ -1261,8 +1261,8 @@ async def wait_for( event: Literal['private_channel_update'], /, *, - check: Optional[Callable[[GroupChannel, GroupChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GroupChannel, GroupChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GroupChannel, GroupChannel]: ... @@ -1272,8 +1272,8 @@ async def wait_for( event: Literal['private_channel_pins_update'], /, *, - check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[PrivateChannel, datetime.datetime]: ... @@ -1283,8 +1283,8 @@ async def wait_for( event: Literal['guild_channel_delete', 'guild_channel_create'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1294,8 +1294,8 @@ async def wait_for( event: Literal['guild_channel_update'], /, *, - check: Optional[Callable[[GuildChannel, GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel, GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GuildChannel, GuildChannel]: ... @@ -1311,7 +1311,7 @@ async def wait_for( bool, ] ], - timeout: Optional[float] = None, + timeout: Optional[float] = ..., ) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]: ... @@ -1321,8 +1321,8 @@ async def wait_for( event: Literal['typing'], /, *, - check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Messageable, Union[User, Member], datetime.datetime]: ... @@ -1332,8 +1332,8 @@ async def wait_for( event: Literal['raw_typing'], /, *, - check: Optional[Callable[[RawTypingEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawTypingEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawTypingEvent: ... @@ -1345,8 +1345,8 @@ async def wait_for( event: Literal['connect', 'disconnect', 'ready', 'resumed'], /, *, - check: Optional[Callable[[], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[], bool]] = ..., + timeout: Optional[float] = ..., ) -> None: ... @@ -1356,8 +1356,8 @@ async def wait_for( event: Literal['shard_connect', 'shard_disconnect', 'shard_ready', 'shard_resumed'], /, *, - check: Optional[Callable[[int], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[int], bool]] = ..., + timeout: Optional[float] = ..., ) -> int: ... @@ -1367,8 +1367,8 @@ async def wait_for( event: Literal['socket_event_type', 'socket_raw_receive'], /, *, - check: Optional[Callable[[str], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[str], bool]] = ..., + timeout: Optional[float] = ..., ) -> str: ... @@ -1378,8 +1378,8 @@ async def wait_for( event: Literal['socket_raw_send'], /, *, - check: Optional[Callable[[Union[str, bytes]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Union[str, bytes]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Union[str, bytes]: ... @@ -1390,8 +1390,8 @@ async def wait_for( event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'], /, *, - check: Optional[Callable[[Entitlement], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Entitlement], bool]] = ..., + timeout: Optional[float] = ..., ) -> Entitlement: ... @@ -1408,8 +1408,8 @@ async def wait_for( ], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1419,8 +1419,8 @@ async def wait_for( event: Literal['guild_update'], /, *, - check: Optional[Callable[[Guild, Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Guild]: ... @@ -1430,8 +1430,8 @@ async def wait_for( event: Literal['guild_emojis_update'], /, *, - check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]: ... @@ -1441,8 +1441,8 @@ async def wait_for( event: Literal['guild_stickers_update'], /, *, - check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]: ... @@ -1452,8 +1452,8 @@ async def wait_for( event: Literal['invite_create', 'invite_delete'], /, *, - check: Optional[Callable[[Invite], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Invite], bool]] = ..., + timeout: Optional[float] = ..., ) -> Invite: ... @@ -1463,8 +1463,8 @@ async def wait_for( event: Literal['audit_log_entry_create'], /, *, - check: Optional[Callable[[AuditLogEntry], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AuditLogEntry], bool]] = ..., + timeout: Optional[float] = ..., ) -> AuditLogEntry: ... @@ -1476,8 +1476,8 @@ async def wait_for( event: Literal['integration_create', 'integration_update'], /, *, - check: Optional[Callable[[Integration], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Integration], bool]] = ..., + timeout: Optional[float] = ..., ) -> Integration: ... @@ -1487,8 +1487,8 @@ async def wait_for( event: Literal['guild_integrations_update'], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1498,8 +1498,8 @@ async def wait_for( event: Literal['webhooks_update'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1509,8 +1509,8 @@ async def wait_for( event: Literal['raw_integration_delete'], /, *, - check: Optional[Callable[[RawIntegrationDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawIntegrationDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawIntegrationDeleteEvent: ... @@ -1522,8 +1522,8 @@ async def wait_for( event: Literal['interaction'], /, *, - check: Optional[Callable[[Interaction[Self]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Interaction[Self]: ... @@ -1535,8 +1535,8 @@ async def wait_for( event: Literal['member_join', 'member_remove'], /, *, - check: Optional[Callable[[Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Member: ... @@ -1546,8 +1546,8 @@ async def wait_for( event: Literal['raw_member_remove'], /, *, - check: Optional[Callable[[RawMemberRemoveEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMemberRemoveEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMemberRemoveEvent: ... @@ -1557,8 +1557,8 @@ async def wait_for( event: Literal['member_update', 'presence_update'], /, *, - check: Optional[Callable[[Member, Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, Member]: ... @@ -1568,8 +1568,8 @@ async def wait_for( event: Literal['user_update'], /, *, - check: Optional[Callable[[User, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[User, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[User, User]: ... @@ -1579,8 +1579,8 @@ async def wait_for( event: Literal['member_ban'], /, *, - check: Optional[Callable[[Guild, Union[User, Member]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Union[User, Member]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Union[User, Member]]: ... @@ -1590,8 +1590,8 @@ async def wait_for( event: Literal['member_unban'], /, *, - check: Optional[Callable[[Guild, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, User]: ... @@ -1603,8 +1603,8 @@ async def wait_for( event: Literal['message', 'message_delete'], /, *, - check: Optional[Callable[[Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Message: ... @@ -1614,8 +1614,8 @@ async def wait_for( event: Literal['message_edit'], /, *, - check: Optional[Callable[[Message, Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, Message]: ... @@ -1625,8 +1625,8 @@ async def wait_for( event: Literal['bulk_message_delete'], /, *, - check: Optional[Callable[[List[Message]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[List[Message]], bool]] = ..., + timeout: Optional[float] = ..., ) -> List[Message]: ... @@ -1636,8 +1636,8 @@ async def wait_for( event: Literal['raw_message_edit'], /, *, - check: Optional[Callable[[RawMessageUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageUpdateEvent: ... @@ -1647,8 +1647,8 @@ async def wait_for( event: Literal['raw_message_delete'], /, *, - check: Optional[Callable[[RawMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageDeleteEvent: ... @@ -1658,8 +1658,8 @@ async def wait_for( event: Literal['raw_bulk_message_delete'], /, *, - check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawBulkMessageDeleteEvent: ... @@ -1671,8 +1671,8 @@ async def wait_for( event: Literal['reaction_add', 'reaction_remove'], /, *, - check: Optional[Callable[[Reaction, Union[Member, User]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction, Union[Member, User]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Reaction, Union[Member, User]]: ... @@ -1682,8 +1682,8 @@ async def wait_for( event: Literal['reaction_clear'], /, *, - check: Optional[Callable[[Message, List[Reaction]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, List[Reaction]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, List[Reaction]]: ... @@ -1693,8 +1693,8 @@ async def wait_for( event: Literal['reaction_clear_emoji'], /, *, - check: Optional[Callable[[Reaction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction], bool]] = ..., + timeout: Optional[float] = ..., ) -> Reaction: ... @@ -1704,8 +1704,8 @@ async def wait_for( event: Literal['raw_reaction_add', 'raw_reaction_remove'], /, *, - check: Optional[Callable[[RawReactionActionEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionActionEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionActionEvent: ... @@ -1715,8 +1715,8 @@ async def wait_for( event: Literal['raw_reaction_clear'], /, *, - check: Optional[Callable[[RawReactionClearEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEvent: ... @@ -1726,8 +1726,8 @@ async def wait_for( event: Literal['raw_reaction_clear_emoji'], /, *, - check: Optional[Callable[[RawReactionClearEmojiEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEmojiEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEmojiEvent: ... @@ -1739,8 +1739,8 @@ async def wait_for( event: Literal['guild_role_create', 'guild_role_delete'], /, *, - check: Optional[Callable[[Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Role: ... @@ -1750,8 +1750,8 @@ async def wait_for( event: Literal['guild_role_update'], /, *, - check: Optional[Callable[[Role, Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role, Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Role, Role]: ... @@ -1763,8 +1763,8 @@ async def wait_for( event: Literal['scheduled_event_create', 'scheduled_event_delete'], /, *, - check: Optional[Callable[[ScheduledEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> ScheduledEvent: ... @@ -1774,8 +1774,8 @@ async def wait_for( event: Literal['scheduled_event_user_add', 'scheduled_event_user_remove'], /, *, - check: Optional[Callable[[ScheduledEvent, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[ScheduledEvent, User]: ... @@ -1787,8 +1787,8 @@ async def wait_for( event: Literal['stage_instance_create', 'stage_instance_delete'], /, *, - check: Optional[Callable[[StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> StageInstance: ... @@ -1798,8 +1798,8 @@ async def wait_for( event: Literal['stage_instance_update'], /, *, - check: Optional[Callable[[StageInstance, StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance, StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]: ... @@ -1810,8 +1810,8 @@ async def wait_for( event: Literal['subscription_create', 'subscription_update', 'subscription_delete'], /, *, - check: Optional[Callable[[Subscription], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Subscription], bool]] = ..., + timeout: Optional[float] = ..., ) -> Subscription: ... @@ -1822,8 +1822,8 @@ async def wait_for( event: Literal['thread_create', 'thread_join', 'thread_remove', 'thread_delete'], /, *, - check: Optional[Callable[[Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Thread: ... @@ -1833,8 +1833,8 @@ async def wait_for( event: Literal['thread_update'], /, *, - check: Optional[Callable[[Thread, Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread, Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Thread, Thread]: ... @@ -1844,8 +1844,8 @@ async def wait_for( event: Literal['raw_thread_update'], /, *, - check: Optional[Callable[[RawThreadUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadUpdateEvent: ... @@ -1855,8 +1855,8 @@ async def wait_for( event: Literal['raw_thread_delete'], /, *, - check: Optional[Callable[[RawThreadDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadDeleteEvent: ... @@ -1866,8 +1866,8 @@ async def wait_for( event: Literal['thread_member_join', 'thread_member_remove'], /, *, - check: Optional[Callable[[ThreadMember], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ThreadMember], bool]] = ..., + timeout: Optional[float] = ..., ) -> ThreadMember: ... @@ -1877,8 +1877,8 @@ async def wait_for( event: Literal['raw_thread_member_remove'], /, *, - check: Optional[Callable[[RawThreadMembersUpdate], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadMembersUpdate], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadMembersUpdate: ... @@ -1890,8 +1890,8 @@ async def wait_for( event: Literal['voice_state_update'], /, *, - check: Optional[Callable[[Member, VoiceState, VoiceState], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, VoiceState, VoiceState], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, VoiceState, VoiceState]: ... @@ -1903,8 +1903,8 @@ async def wait_for( event: Literal['poll_vote_add', 'poll_vote_remove'], /, *, - check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Union[User, Member], PollAnswer]: ... @@ -1914,8 +1914,8 @@ async def wait_for( event: Literal['raw_poll_vote_add', 'raw_poll_vote_remove'], /, *, - check: Optional[Callable[[RawPollVoteActionEvent], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[RawPollVoteActionEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawPollVoteActionEvent: ... @@ -1927,8 +1927,8 @@ async def wait_for( event: Literal["command", "command_completion"], /, *, - check: Optional[Callable[[Context[Any]], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Context[Any]: ... @@ -1938,8 +1938,8 @@ async def wait_for( event: Literal["command_error"], /, *, - check: Optional[Callable[[Context[Any], CommandError], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any], CommandError], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Context[Any], CommandError]: ... @@ -1949,8 +1949,8 @@ async def wait_for( event: str, /, *, - check: Optional[Callable[..., bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[..., bool]] = ..., + timeout: Optional[float] = ..., ) -> Any: ... From a8134dfa1616f51483c83e191d848765303e4fff Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Sun, 9 Feb 2025 03:00:49 +0100 Subject: [PATCH 179/354] Fix Embed type attribute ddocs link --- discord/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/embeds.py b/discord/embeds.py index 258ef0dfd86c..efa6537db4c7 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -131,7 +131,7 @@ class Embed: The type of embed. Usually "rich". This can be set during initialisation. Possible strings for embed types can be found on discord's - :ddocs:`api docs ` + :ddocs:`api docs ` description: Optional[:class:`str`] The description of the embed. This can be set during initialisation. From 42e4a8737446ed171bd9478b5b7bcab44db451ce Mon Sep 17 00:00:00 2001 From: dolfies Date: Sat, 8 Feb 2025 21:05:21 -0500 Subject: [PATCH 180/354] Fix typo in voice server update state handling --- discord/voice_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/voice_state.py b/discord/voice_state.py index f10a307d6715..956f639b8e0a 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -344,7 +344,7 @@ async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: elif self.state is not ConnectionFlowState.disconnected: # eventual consistency - if previous_token == self.token and previous_server_id == self.server_id and previous_token == self.token: + if previous_token == self.token and previous_server_id == self.server_id and previous_endpoint == self.endpoint: return _log.debug('Unexpected server update event, attempting to handle') From 52967ec1034ed02f1a12f7bb0eefd1b8b5660f3d Mon Sep 17 00:00:00 2001 From: Eric Schneider <16943959+tailoric@users.noreply.github.com> Date: Sun, 9 Feb 2025 03:08:45 +0100 Subject: [PATCH 181/354] Fix path sanitation for absolute Windows paths When using an absolute Windows path (e.g. `C:\Users\USER\Documents\`) for the `newbot` command the translation table replaced the valid `:` character in the drive causing it to create the directory at the wrong place. Fixes #10096 --- discord/__main__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/discord/__main__.py b/discord/__main__.py index 843274b53a76..f8556fcdc1ae 100644 --- a/discord/__main__.py +++ b/discord/__main__.py @@ -28,7 +28,7 @@ import argparse import sys -from pathlib import Path +from pathlib import Path, PurePath, PureWindowsPath import discord import importlib.metadata @@ -225,8 +225,14 @@ def to_path(parser: argparse.ArgumentParser, name: str, *, replace_spaces: bool ) if len(name) <= 4 and name.upper() in forbidden: parser.error('invalid directory name given, use a different one') + path = PurePath(name) + if isinstance(path, PureWindowsPath) and path.drive: + drive, rest = path.parts[0], path.parts[1:] + transformed = tuple(map(lambda p: p.translate(_translation_table), rest)) + name = drive + '\\'.join(transformed) - name = name.translate(_translation_table) + else: + name = name.translate(_translation_table) if replace_spaces: name = name.replace(' ', '-') return Path(name) From 76eb12666472ddd4a01459555680e630804994ea Mon Sep 17 00:00:00 2001 From: Gooraeng <101193491+Gooraeng@users.noreply.github.com> Date: Sun, 9 Feb 2025 18:46:28 +0900 Subject: [PATCH 182/354] Add 'mention' property in PartialWebhookChannel --- discord/webhook/async_.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2d9856ae3d0e..2faa9f0e0d6d 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -660,6 +660,11 @@ def __init__(self, *, data: PartialChannelPayload) -> None: def __repr__(self) -> str: return f'' + @property + def mention(self) -> str: + """:class:`str`: The string that allows you to mention the channel that the webhook is following.""" + return f'<#{self.id}>' + class PartialWebhookGuild(Hashable): """Represents a partial guild for webhooks. From 8a95c0190c9e8dee81c7c0a6bf85e8eb4a834f82 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:42:17 +0100 Subject: [PATCH 183/354] Add support for embed flags and update attachment flags Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/embeds.py | 19 ++++++++- discord/flags.py | 89 ++++++++++++++++++++++++++++++++++++++++++ discord/types/embed.py | 2 + docs/api.rst | 8 ++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/discord/embeds.py b/discord/embeds.py index efa6537db4c7..4be644688ee6 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -29,6 +29,7 @@ from . import utils from .colour import Colour +from .flags import AttachmentFlags, EmbedFlags # fmt: off __all__ = ( @@ -76,6 +77,7 @@ class _EmbedMediaProxy(Protocol): proxy_url: Optional[str] height: Optional[int] width: Optional[int] + flags: Optional[AttachmentFlags] class _EmbedVideoProxy(Protocol): url: Optional[str] @@ -146,6 +148,10 @@ class Embed: colour: Optional[Union[:class:`Colour`, :class:`int`]] The colour code of the embed. Aliased to ``color`` as well. This can be set during initialisation. + flags: Optional[:class:`EmbedFlags`] + The flags of this embed. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -162,6 +168,7 @@ class Embed: '_author', '_fields', 'description', + 'flags', ) def __init__( @@ -181,6 +188,7 @@ def __init__( self.type: EmbedType = type self.url: Optional[str] = url self.description: Optional[str] = description + self.flags: Optional[EmbedFlags] = None if self.title is not None: self.title = str(self.title) @@ -245,6 +253,11 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: else: setattr(self, '_' + attr, value) + try: + self.flags = EmbedFlags._from_value(data['flags']) + except KeyError: + pass + return self def copy(self) -> Self: @@ -399,11 +412,15 @@ def image(self) -> _EmbedMediaProxy: - ``proxy_url`` - ``width`` - ``height`` + - ``flags`` If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_image', {})) # type: ignore + data = getattr(self, '_image', {}) + if 'flags' in data: + data['flags'] = AttachmentFlags._from_value(data['flags']) + return EmbedProxy(data) # type: ignore def set_image(self, *, url: Optional[Any]) -> Self: """Sets the image for the embed content. diff --git a/discord/flags.py b/discord/flags.py index de806ba9c046..20f8c5470fee 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -63,6 +63,7 @@ 'RoleFlags', 'AppInstallationType', 'SKUFlags', + 'EmbedFlags', ) BF = TypeVar('BF', bound='BaseFlags') @@ -2173,6 +2174,30 @@ def remix(self): """:class:`bool`: Returns ``True`` if the attachment has been edited using the remix feature.""" return 1 << 2 + @flag_value + def spoiler(self): + """:class:`bool`: Returns ``True`` if the attachment was marked as a spoiler. + + .. versionadded:: 2.5 + """ + return 1 << 3 + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def animated(self): + """:class:`bool`: Returns ``True`` if the attachment is an animated image. + + .. versionadded:: 2.5 + """ + return 1 << 5 + @fill_with_flags() class RoleFlags(BaseFlags): @@ -2308,3 +2333,67 @@ def guild_subscription(self): def user_subscription(self): """:class:`bool`: Returns ``True`` if the SKU is a user subscription.""" return 1 << 8 + + +@fill_with_flags() +class EmbedFlags(BaseFlags): + r"""Wraps up the Discord Embed flags + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two EmbedFlags are equal. + + .. describe:: x != y + + Checks if two EmbedFlags are not equal. + + .. describe:: x | y, x |= y + + Returns an EmbedFlags instance with all enabled flags from + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an EmbedFlags instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an EmbedFlags instance with all flags inverted from x. + + .. describe:: hash(x) + + Returns the flag's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the embed was flagged as sensitive content.""" + return 1 << 4 + + @flag_value + def content_inventory_entry(self): + """:class:`bool`: Returns ``True`` if the embed is a reply to an activity card, and is no + longer displayed. + """ + return 1 << 5 diff --git a/discord/types/embed.py b/discord/types/embed.py index 376df3a1a7d0..f8354a3f3069 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -50,6 +50,7 @@ class EmbedVideo(TypedDict, total=False): proxy_url: str height: int width: int + flags: int class EmbedImage(TypedDict, total=False): @@ -88,3 +89,4 @@ class Embed(TypedDict, total=False): provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] + flags: int diff --git a/docs/api.rst b/docs/api.rst index 8da2ba80c894..0b2edcd3db24 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5708,6 +5708,14 @@ SKUFlags .. autoclass:: SKUFlags() :members: +EmbedFlags +~~~~~~~~~~ + +.. attributetable:: EmbedFlags + +.. autoclass:: EmbedFlags() + :members: + ForumTag ~~~~~~~~~ From 4c3ce8fb85e5c901589d056996d6f81dee8dda4b Mon Sep 17 00:00:00 2001 From: Dep <70801324+Depreca1ed@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:13:54 +0530 Subject: [PATCH 184/354] Fix Member.roles having None members Signed-off-by: Depreca1ed <70801324+Depreca1ed@users.noreply.github.com> --- discord/member.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/member.py b/discord/member.py index 2de8fbfc1f34..8dee3354608a 100644 --- a/discord/member.py +++ b/discord/member.py @@ -561,7 +561,9 @@ def roles(self) -> List[Role]: role = g.get_role(role_id) if role: result.append(role) - result.append(g.default_role) + default_role = g.default_role + if default_role: + result.append(default_role) result.sort() return result From 8edf4332557c3b980cf4eabc4bed4cbab9a01934 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 12 Feb 2025 11:49:50 +0100 Subject: [PATCH 185/354] Implement rich role.move interface Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/role.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 8530d4a90a46..571ab9f20bbf 100644 --- a/discord/role.py +++ b/discord/role.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Union, overload, TYPE_CHECKING from .asset import Asset from .permissions import Permissions @@ -522,6 +522,112 @@ async def edit( data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) return Role(guild=self.guild, data=data, state=self._state) + @overload + async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...): + ... + + @overload + async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...): + ... + + async def move( + self, + *, + beginning: bool = MISSING, + end: bool = MISSING, + above: Role = MISSING, + below: Role = MISSING, + offset: int = 0, + reason: Optional[str] = None, + ): + """|coro| + + A rich interface to help move a role relative to other roles. + + You must have :attr:`~discord.Permissions.manage_roles` to do this, + and you cannot move roles above the client's top role in the guild. + + .. versionadded:: 2.5 + + Parameters + ----------- + beginning: :class:`bool` + Whether to move this at the beginning of the role list, above the default role. + This is mutually exclusive with `end`, `above`, and `below`. + end: :class:`bool` + Whether to move this at the end of the role list. + This is mutually exclusive with `beginning`, `above`, and `below`. + above: :class:`Role` + The role that should be above our current role. + This mutually exclusive with `beginning`, `end`, and `below`. + below: :class:`Role` + The role that should be below our current role. + This mutually exclusive with `beginning`, `end`, and `above`. + offset: :class:`int` + The number of roles to offset the move by. For example, + an offset of ``2`` with ``beginning=True`` would move + it 2 above the beginning. A positive number moves it above + while a negative number moves it below. Note that this + number is relative and computed after the ``beginning``, + ``end``, ``before``, and ``after`` parameters. + reason: Optional[:class:`str`] + The reason for editing this role. Shows up on the audit log. + + Raises + ------- + Forbidden + You cannot move the role there, or lack permissions to do so. + HTTPException + Moving the role failed. + TypeError + A bad mix of arguments were passed. + ValueError + An invalid role was passed. + + Returns + -------- + List[:class:`Role`] + A list of all the roles in the guild. + """ + if sum(bool(a) for a in (beginning, end, above, below)) > 1: + raise TypeError('Only one of [beginning, end, above, below] can be used.') + + target = above or below + guild = self.guild + guild_roles = guild.roles + + if target: + if target not in guild_roles: + raise ValueError('Target role is from a different guild') + if above == guild.default_role: + raise ValueError('Role cannot be moved below the default role') + if self == target: + raise ValueError('Target role cannot be itself') + + roles = [r for r in guild_roles if r != self] + if beginning: + index = 1 + elif end: + index = len(roles) + elif above in roles: + index = roles.index(above) + elif below in roles: + index = roles.index(below) + 1 + else: + index = guild_roles.index(self) + roles.insert(max((index + offset), 1), self) + + payload: List[RolePositionUpdate] = [{'id': role.id, 'position': idx} for idx, role in enumerate(roles)] + await self._state.http.move_role_position(guild.id, payload, reason=reason) + async def delete(self, *, reason: Optional[str] = None) -> None: """|coro| From 6ab747f9e5a67ecc28aaaac519a3de8cb2b967d8 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:37:20 +0100 Subject: [PATCH 186/354] Add support for sending views in stateless webhooks --- discord/ui/view.py | 5 +++++ discord/webhook/async_.py | 24 ++++++++++++++---------- discord/webhook/sync.py | 26 +++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 2341a720fef6..ad5ea058536f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -214,6 +214,11 @@ async def __timeout_task_impl(self) -> None: # Wait N seconds to see if timeout data has been refreshed await asyncio.sleep(self.__timeout_expiry - now) + def is_dispatchable(self) -> bool: + # this is used by webhooks to check whether a view requires a state attached + # or not, this simply is, whether a view has a component other than a url button + return any(item.is_dispatchable() for item in self.children) + def to_components(self) -> List[Dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2faa9f0e0d6d..1966832e0f0a 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -310,8 +310,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> Response[Optional[MessagePayload]]: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -1715,10 +1716,9 @@ async def send( .. versionadded:: 1.4 view: :class:`discord.ui.View` - The view to send with the message. You can only send a view - if this webhook is not partial and has state attached. A - webhook has state attached if the webhook is managed by the - library. + The view to send with the message. If the webhook is partial or + is not managed by the library, then you can only send URL buttons. + Otherwise, you can send views with any type of components. .. versionadded:: 2.0 thread: :class:`~discord.abc.Snowflake` @@ -1770,7 +1770,8 @@ async def send( The length of ``embeds`` was invalid, there was no token associated with this webhook or ``ephemeral`` was passed with the improper webhook type or there was no state - attached with this webhook when giving it a view. + attached with this webhook when giving it a view that had + components other than URL buttons. Returns --------- @@ -1800,13 +1801,15 @@ async def send( wait = True if view is not MISSING: - if isinstance(self._state, _WebhookState): - raise ValueError('Webhook views require an associated state with the webhook') - if not hasattr(view, '__discord_ui_view__'): raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') - if ephemeral is True and view.timeout is None: + if isinstance(self._state, _WebhookState) and view.is_dispatchable(): + raise ValueError( + 'Webhook views with any component other than URL buttons require an associated state with the webhook' + ) + + if ephemeral is True and view.timeout is None and view.is_dispatchable(): view.timeout = 15 * 60.0 if thread_name is not MISSING and thread is not MISSING: @@ -1850,6 +1853,7 @@ async def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) msg = None diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index cf23e977b33a..171931b12ea2 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -66,6 +66,7 @@ from ..message import Attachment from ..abc import Snowflake from ..state import ConnectionState + from ..ui import View from ..types.webhook import ( Webhook as WebhookPayload, ) @@ -290,8 +291,9 @@ def execute_webhook( files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, wait: bool = False, + with_components: bool = False, ) -> MessagePayload: - params = {'wait': int(wait)} + params = {'wait': int(wait), 'with_components': int(with_components)} if thread_id: params['thread_id'] = thread_id route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token) @@ -919,6 +921,7 @@ def send( silent: bool = False, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, + view: View = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. @@ -991,6 +994,13 @@ def send( When sending a Poll via webhook, you cannot manually end it. .. versionadded:: 2.4 + view: :class:`~discord.ui.View` + The view to send with the message. This can only have URL buttons, which donnot + require a state to be attached to it. + + If you want to send a view with any component attached to it, check :meth:`Webhook.send`. + + .. versionadded:: 2.5 Raises -------- @@ -1004,8 +1014,9 @@ def send( You specified both ``embed`` and ``embeds`` or ``file`` and ``files`` or ``thread`` and ``thread_name``. ValueError - The length of ``embeds`` was invalid or - there was no token associated with this webhook. + The length of ``embeds`` was invalid, there was no token + associated with this webhook or you tried to send a view + with components other than URL buttons. Returns --------- @@ -1027,6 +1038,13 @@ def send( else: flags = MISSING + if view is not MISSING: + if not hasattr(view, '__discord_ui_view__'): + raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}') + + if view.is_dispatchable(): + raise ValueError('SyncWebhook views can only contain URL buttons') + if thread_name is not MISSING and thread is not MISSING: raise TypeError('Cannot mix thread_name and thread keyword arguments.') @@ -1050,6 +1068,7 @@ def send( flags=flags, applied_tags=applied_tag_ids, poll=poll, + view=view, ) as params: adapter: WebhookAdapter = _get_webhook_adapter() thread_id: Optional[int] = None @@ -1065,6 +1084,7 @@ def send( files=params.files, thread_id=thread_id, wait=wait, + with_components=view is not MISSING, ) msg = None From fa158a5eba44d49d9dac063e296800e0e3bb58ae Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:21:19 +0100 Subject: [PATCH 187/354] Add support for getting and editing integration_types_config application field --- discord/appinfo.py | 129 ++++++++++++++++++++++++++++++++++++++- discord/http.py | 1 + discord/types/appinfo.py | 7 ++- docs/api.rst | 8 +++ 4 files changed, 141 insertions(+), 4 deletions(-) diff --git a/discord/appinfo.py b/discord/appinfo.py index 932f852c2d79..990c7c2fe356 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -24,7 +24,7 @@ from __future__ import annotations -from typing import List, TYPE_CHECKING, Optional +from typing import List, TYPE_CHECKING, Literal, Optional from . import utils from .asset import Asset @@ -41,6 +41,7 @@ PartialAppInfo as PartialAppInfoPayload, Team as TeamPayload, InstallParams as InstallParamsPayload, + AppIntegrationTypeConfig as AppIntegrationTypeConfigPayload, ) from .user import User from .state import ConnectionState @@ -49,6 +50,7 @@ 'AppInfo', 'PartialAppInfo', 'AppInstallParams', + 'IntegrationTypeConfig', ) @@ -180,6 +182,7 @@ class AppInfo: 'redirect_uris', 'approximate_guild_count', 'approximate_user_install_count', + '_integration_types_config', ) def __init__(self, state: ConnectionState, data: AppInfoPayload): @@ -218,6 +221,9 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.redirect_uris: List[str] = data.get('redirect_uris', []) self.approximate_guild_count: int = data.get('approximate_guild_count', 0) self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count') + self._integration_types_config: Dict[Literal['0', '1'], AppIntegrationTypeConfigPayload] = data.get( + 'integration_types_config', {} + ) def __repr__(self) -> str: return ( @@ -260,6 +266,36 @@ def flags(self) -> ApplicationFlags: """ return ApplicationFlags._from_value(self._flags) + @property + def guild_integration_config(self) -> Optional[IntegrationTypeConfig]: + """Optional[:class:`IntegrationTypeConfig`]: The default settings for the + application's installation context in a guild. + + .. versionadded:: 2.5 + """ + if not self._integration_types_config: + return None + + try: + return IntegrationTypeConfig(self._integration_types_config['0']) + except KeyError: + return None + + @property + def user_integration_config(self) -> Optional[IntegrationTypeConfig]: + """Optional[:class:`IntegrationTypeConfig`]: The default settings for the + application's installation context as a user. + + .. versionadded:: 2.5 + """ + if not self._integration_types_config: + return None + + try: + return IntegrationTypeConfig(self._integration_types_config['1']) + except KeyError: + return None + async def edit( self, *, @@ -274,6 +310,10 @@ async def edit( cover_image: Optional[bytes] = MISSING, interactions_endpoint_url: Optional[str] = MISSING, tags: Optional[List[str]] = MISSING, + guild_install_scopes: Optional[List[str]] = MISSING, + guild_install_permissions: Optional[Permissions] = MISSING, + user_install_scopes: Optional[List[str]] = MISSING, + user_install_permissions: Optional[Permissions] = MISSING, ) -> AppInfo: r"""|coro| @@ -315,6 +355,24 @@ async def edit( over the gateway. Can be ``None`` to remove the URL. tags: Optional[List[:class:`str`]] The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags. + guild_install_scopes: Optional[List[:class:`str`]] + The new list of :ddocs:`OAuth2 scopes ` of + the default guild installation context. Can be ``None`` to remove the scopes. + + .. versionadded: 2.5 + guild_install_permissions: Optional[:class:`Permissions`] + The new permissions of the default guild installation context. Can be ``None`` to remove the permissions. + + .. versionadded: 2.5 + user_install_scopes: Optional[List[:class:`str`]] + The new list of :ddocs:`OAuth2 scopes ` of + the default user installation context. Can be ``None`` to remove the scopes. + + .. versionadded: 2.5 + user_install_permissions: Optional[:class:`Permissions`] + The new permissions of the default user installation context. Can be ``None`` to remove the permissions. + + .. versionadded: 2.5 reason: Optional[:class:`str`] The reason for editing the application. Shows up on the audit log. @@ -324,7 +382,8 @@ async def edit( Editing the application failed ValueError The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised - when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other. + when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other, + or when ``guild_install_scopes`` and ``guild_install_permissions`` are incompatible with each other. Returns ------- @@ -364,7 +423,7 @@ async def edit( else: if install_params_permissions is not MISSING: - raise ValueError("install_params_scopes must be set if install_params_permissions is set") + raise ValueError('install_params_scopes must be set if install_params_permissions is set') if flags is not MISSING: if flags is None: @@ -389,6 +448,51 @@ async def edit( if tags is not MISSING: payload['tags'] = tags + + integration_types_config: Dict[str, Any] = {} + if guild_install_scopes is not MISSING or guild_install_permissions is not MISSING: + guild_install_params: Optional[Dict[str, Any]] = {} + if guild_install_scopes in (None, MISSING): + guild_install_scopes = [] + + if 'bot' not in guild_install_scopes and guild_install_permissions is not MISSING: + raise ValueError("'bot' must be in guild_install_scopes if guild_install_permissions is set") + + if guild_install_permissions in (None, MISSING): + guild_install_params['permissions'] = 0 + else: + guild_install_params['permissions'] = guild_install_permissions.value + + guild_install_params['scopes'] = guild_install_scopes + + integration_types_config['0'] = {'oauth2_install_params': guild_install_params or None} + else: + if guild_install_permissions is not MISSING: + raise ValueError('guild_install_scopes must be set if guild_install_permissions is set') + + if user_install_scopes is not MISSING or user_install_permissions is not MISSING: + user_install_params: Optional[Dict[str, Any]] = {} + if user_install_scopes in (None, MISSING): + user_install_scopes = [] + + if 'bot' not in user_install_scopes and user_install_permissions is not MISSING: + raise ValueError("'bot' must be in user_install_scopes if user_install_permissions is set") + + if user_install_permissions in (None, MISSING): + user_install_params['permissions'] = 0 + else: + user_install_params['permissions'] = user_install_permissions.value + + user_install_params['scopes'] = user_install_scopes + + integration_types_config['1'] = {'oauth2_install_params': user_install_params or None} + else: + if user_install_permissions is not MISSING: + raise ValueError('user_install_scopes must be set if user_install_permissions is set') + + if integration_types_config: + payload['integration_types_config'] = integration_types_config + data = await self._state.http.edit_application_info(reason=reason, payload=payload) return AppInfo(data=data, state=self._state) @@ -520,3 +624,22 @@ class AppInstallParams: def __init__(self, data: InstallParamsPayload) -> None: self.scopes: List[str] = data.get('scopes', []) self.permissions: Permissions = Permissions(int(data['permissions'])) + + +class IntegrationTypeConfig: + """Represents the default settings for the application's installation context. + + .. versionadded:: 2.5 + + Attributes + ---------- + oauth2_install_params: Optional[:class:`AppInstallParams`] + The install params for this installation context's default in-app authorization link. + """ + + def __init__(self, data: AppIntegrationTypeConfigPayload) -> None: + self.oauth2_install_params: Optional[AppInstallParams] = None + try: + self.oauth2_install_params = AppInstallParams(data['oauth2_install_params']) # type: ignore # EAFP + except KeyError: + pass diff --git a/discord/http.py b/discord/http.py index fd0acae3713d..6617efa2708b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2620,6 +2620,7 @@ def edit_application_info(self, *, reason: Optional[str], payload: Any) -> Respo 'cover_image', 'interactions_endpoint_url ', 'tags', + 'integration_types_config', ) payload = {k: v for k, v in payload.items() if k in valid_keys} diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index 7cca955b7986..9452bbbb150d 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -24,7 +24,7 @@ from __future__ import annotations -from typing import TypedDict, List, Optional +from typing import Literal, Dict, TypedDict, List, Optional from typing_extensions import NotRequired from .user import User @@ -38,6 +38,10 @@ class InstallParams(TypedDict): permissions: str +class AppIntegrationTypeConfig(TypedDict): + oauth2_install_params: NotRequired[InstallParams] + + class BaseAppInfo(TypedDict): id: Snowflake name: str @@ -69,6 +73,7 @@ class AppInfo(BaseAppInfo): tags: NotRequired[List[str]] install_params: NotRequired[InstallParams] custom_install_url: NotRequired[str] + integration_types_config: NotRequired[Dict[Literal['0', '1'], AppIntegrationTypeConfig]] class PartialAppInfo(BaseAppInfo, total=False): diff --git a/docs/api.rst b/docs/api.rst index 0b2edcd3db24..934335c5ac70 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -80,6 +80,14 @@ AppInstallParams .. autoclass:: AppInstallParams() :members: +IntegrationTypeConfig +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: IntegrationTypeConfig + +.. autoclass:: IntegrationTypeConfig() + :members: + Team ~~~~~ From 5b78097cef292ba394b3f95c5157d2251b74ae78 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:10:59 +0100 Subject: [PATCH 188/354] Add support for Interaction Callback Resource Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/interactions.py | 213 ++++++++++++++++++++++++++++++++-- discord/types/interactions.py | 34 ++++++ discord/webhook/async_.py | 16 ++- docs/interactions/api.rst | 16 +++ 4 files changed, 269 insertions(+), 10 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 49bfbfb07203..07c600a5dde0 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -54,6 +54,8 @@ 'Interaction', 'InteractionMessage', 'InteractionResponse', + 'InteractionCallback', + 'InteractionCallbackActivityInstance', ) if TYPE_CHECKING: @@ -61,6 +63,8 @@ Interaction as InteractionPayload, InteractionData, ApplicationCommandInteractionData, + InteractionCallback as InteractionCallbackPayload, + InteractionCallbackActivity as InteractionCallbackActivityPayload, ) from .types.webhook import ( Webhook as WebhookPayload, @@ -90,6 +94,10 @@ DMChannel, GroupChannel, ] + InteractionCallbackResource = Union[ + "InteractionMessage", + "InteractionCallbackActivityInstance", + ] MISSING: Any = utils.MISSING @@ -469,6 +477,7 @@ async def edit_original_response( attachments: Sequence[Union[Attachment, File]] = MISSING, view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -503,6 +512,14 @@ async def edit_original_response( view: Optional[:class:`~discord.ui.View`] The updated view to update this message with. If ``None`` is passed then the view is removed. + poll: :class:`Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted when the response type is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -532,6 +549,7 @@ async def edit_original_response( view=view, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, + poll=poll, ) as params: adapter = async_context.get() http = self._state.http @@ -624,6 +642,106 @@ async def translate( return await translator.translate(string, locale=locale, context=context) +class InteractionCallbackActivityInstance: + """Represents an activity instance launched as an interaction response. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`str` + The activity instance ID. + """ + + __slots__ = ('id',) + + def __init__(self, data: InteractionCallbackActivityPayload) -> None: + self.id: str = data['id'] + + +class InteractionCallback(Generic[ClientT]): + """Represents an interaction response callback. + + .. versionadded:: 2.5 + + Attributes + ---------- + id: :class:`int` + The interaction ID. + type: :class:`InteractionResponseType` + The interaction callback response type. + resource: Optional[Union[:class:`InteractionMessage`, :class:`InteractionCallbackActivityInstance`]] + The resource that the interaction response created. If a message was sent, this will be + a :class:`InteractionMessage`. If an activity was launched this will be a + :class:`InteractionCallbackActivityInstance`. In any other case, this will be ``None``. + message_id: Optional[:class:`int`] + The message ID of the resource. Only available if the resource is a :class:`InteractionMessage`. + activity_id: Optional[:class:`str`] + The activity ID of the resource. Only available if the resource is a :class:`InteractionCallbackActivityInstance`. + """ + + __slots__ = ( + '_state', + '_parent', + 'type', + 'id', + '_thinking', + '_ephemeral', + 'message_id', + 'activity_id', + 'resource', + ) + + def __init__( + self, + *, + data: InteractionCallbackPayload, + parent: Interaction[ClientT], + state: ConnectionState, + type: InteractionResponseType, + ) -> None: + self._state: ConnectionState = state + self._parent: Interaction[ClientT] = parent + self.type: InteractionResponseType = type + self._update(data) + + def _update(self, data: InteractionCallbackPayload) -> None: + interaction = data['interaction'] + + self.id: int = int(interaction['id']) + self._thinking: bool = interaction.get('response_message_loading', False) + self._ephemeral: bool = interaction.get('response_message_ephemeral', False) + + self.message_id: Optional[int] = utils._get_as_snowflake(interaction, 'response_message_id') + self.activity_id: Optional[str] = interaction.get('activity_instance_id') + + self.resource: Optional[InteractionCallbackResource] = None + + resource = data.get('resource') + if resource is not None: + + self.type = try_enum(InteractionResponseType, resource['type']) + + message = resource.get('message') + activity_instance = resource.get('activity_instance') + if message is not None: + self.resource = InteractionMessage( + state=self._state, + channel=self._parent.channel, # type: ignore # channel should be the correct type here + data=message, + ) + elif activity_instance is not None: + self.resource = InteractionCallbackActivityInstance(activity_instance) + + def is_thinking(self) -> bool: + """:class:`bool`: Whether the response was a thinking defer.""" + return self._thinking + + def is_ephemeral(self) -> bool: + """:class:`bool`: Whether the response was ephemeral.""" + return self._ephemeral + + class InteractionResponse(Generic[ClientT]): """Represents a Discord interaction response. @@ -653,7 +771,12 @@ def type(self) -> Optional[InteractionResponseType]: """:class:`InteractionResponseType`: The type of response that was sent, ``None`` if response is not done.""" return self._response_type - async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> None: + async def defer( + self, + *, + ephemeral: bool = False, + thinking: bool = False, + ) -> Optional[InteractionCallback[ClientT]]: """|coro| Defers the interaction response. @@ -667,6 +790,9 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non - :attr:`InteractionType.component` - :attr:`InteractionType.modal_submit` + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- ephemeral: :class:`bool` @@ -685,6 +811,11 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non Deferring the interaction failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallback`] + The interaction callback resource, or ``None``. """ if self._response_type: raise InteractionResponded(self._parent) @@ -709,7 +840,7 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non adapter = async_context.get() params = interaction_response_params(type=defer_type, data=data) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -718,6 +849,12 @@ async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> Non params=params, ) self._response_type = InteractionResponseType(defer_type) + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) async def pong(self) -> None: """|coro| @@ -767,11 +904,14 @@ async def send_message( silent: bool = False, delete_after: Optional[float] = None, poll: Poll = MISSING, - ) -> None: + ) -> InteractionCallback[ClientT]: """|coro| Responds to this interaction by sending a message. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -825,6 +965,11 @@ async def send_message( The length of ``embeds`` was invalid. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallback` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -855,7 +1000,7 @@ async def send_message( ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -886,6 +1031,13 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def edit_message( self, *, @@ -897,12 +1049,15 @@ async def edit_message( allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, - ) -> None: + ) -> Optional[InteractionCallback[ClientT]]: """|coro| Responds to this interaction by editing the original message of a component or modal interaction. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -948,6 +1103,11 @@ async def edit_message( You specified both ``embed`` and ``embeds``. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallback`] + The interaction callback data, or ``None`` if editing the message was not possible. """ if self._response_type: raise InteractionResponded(self._parent) @@ -990,7 +1150,7 @@ async def edit_message( ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1015,15 +1175,29 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) - async def send_modal(self, modal: Modal, /) -> None: + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + + async def send_modal(self, modal: Modal, /) -> InteractionCallback[ClientT]: """|coro| Responds to this interaction by sending a modal. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallback` instance. + Parameters ----------- modal: :class:`~discord.ui.Modal` The modal to send. + with_response: :class:`bool` + Whether to return the interaction response callback resource. + + .. versionadded:: 2.5 Raises ------- @@ -1031,6 +1205,11 @@ async def send_modal(self, modal: Modal, /) -> None: Sending the modal failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallback` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -1041,7 +1220,7 @@ async def send_modal(self, modal: Modal, /) -> None: http = parent._state.http params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict()) - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1053,6 +1232,13 @@ async def send_modal(self, modal: Modal, /) -> None: self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal + return InteractionCallback( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None: """|coro| @@ -1154,6 +1340,7 @@ async def edit( view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, delete_after: Optional[float] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -1188,6 +1375,15 @@ async def edit( then it is silently ignored. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted if the interaction response's :attr:`InteractionResponse.type` + attribute is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -1212,6 +1408,7 @@ async def edit( attachments=attachments, view=view, allowed_mentions=allowed_mentions, + poll=poll, ) if delete_after is not None: await self.delete(delay=delete_after) diff --git a/discord/types/interactions.py b/discord/types/interactions.py index a72a5b2cea15..3f3516c3a696 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -42,6 +42,16 @@ InteractionType = Literal[1, 2, 3, 4, 5] +InteractionResponseType = Literal[ + 1, + 4, + 5, + 6, + 7, + 8, + 9, + 10, +] InteractionContextType = Literal[0, 1, 2] InteractionInstallationType = Literal[0, 1] @@ -301,3 +311,27 @@ class ModalSubmitMessageInteractionMetadata(_MessageInteractionMetadata): MessageComponentMessageInteractionMetadata, ModalSubmitMessageInteractionMetadata, ] + + +class InteractionCallbackResponse(TypedDict): + id: Snowflake + type: InteractionType + activity_instance_id: NotRequired[str] + response_message_id: NotRequired[Snowflake] + response_message_loading: NotRequired[bool] + response_message_ephemeral: NotRequired[bool] + + +class InteractionCallbackActivity(TypedDict): + id: str + + +class InteractionCallbackResource(TypedDict): + type: InteractionResponseType + activity_instance: NotRequired[InteractionCallbackActivity] + message: NotRequired[Message] + + +class InteractionCallback(TypedDict): + interaction: InteractionCallbackResponse + resource: NotRequired[InteractionCallbackResource] diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 1966832e0f0a..3b62b10faa2c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -90,6 +90,9 @@ ) from ..types.emoji import PartialEmoji as PartialEmojiPayload from ..types.snowflake import SnowflakeList + from ..types.interactions import ( + InteractionCallback as InteractionCallbackResponsePayload, + ) BE = TypeVar('BE', bound=BaseException) _State = Union[ConnectionState, '_WebhookState'] @@ -435,13 +438,14 @@ def create_interaction_response( proxy: Optional[str] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, params: MultipartParameters, - ) -> Response[None]: + ) -> Response[InteractionCallbackResponsePayload]: route = Route( 'POST', '/interactions/{webhook_id}/{webhook_token}/callback', webhook_id=interaction_id, webhook_token=token, ) + request_params = {'with_response': '1'} if params.files: return self.request( @@ -451,9 +455,17 @@ def create_interaction_response( proxy_auth=proxy_auth, files=params.files, multipart=params.multipart, + params=request_params, ) else: - return self.request(route, session=session, proxy=proxy, proxy_auth=proxy_auth, payload=params.payload) + return self.request( + route, + session=session, + proxy=proxy, + proxy_auth=proxy_auth, + payload=params.payload, + params=request_params, + ) def get_original_interaction_response( self, diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index aeb6a25c613d..0bf69903bfc7 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -28,6 +28,22 @@ InteractionResponse .. autoclass:: InteractionResponse() :members: +InteractionCallback +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallback + +.. autoclass:: InteractionCallback() + :members: + +InteractionCallbackActivityInstance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionCallbackActivityInstance + +.. autoclass:: InteractionCallbackActivityInstance() + :members: + InteractionMessage ~~~~~~~~~~~~~~~~~~~ From 776fc2251dd2ce1652dffe85f134d7937371e5ab Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 02:21:45 -0500 Subject: [PATCH 189/354] [commands] Use interaction response within Context.send --- discord/ext/commands/context.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 5a74fa5f3a9e..fa89b5078503 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -1078,8 +1078,11 @@ async def send( if self.interaction.response.is_done(): msg = await self.interaction.followup.send(**kwargs, wait=True) else: - await self.interaction.response.send_message(**kwargs) - msg = await self.interaction.original_response() + response = await self.interaction.response.send_message(**kwargs) + if not isinstance(response.resource, discord.InteractionMessage): + msg = await self.interaction.original_response() + else: + msg = response.resource if delete_after is not None: await msg.delete(delay=delete_after) From 1cdf71090810ce5aa65da66285fc8790cea56f77 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 02:40:25 -0500 Subject: [PATCH 190/354] Rename InteractionCallback to InteractionCallbackResponse --- discord/interactions.py | 36 ++++++++++++++++++------------------ docs/interactions/api.rst | 8 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 07c600a5dde0..11cb9792988b 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -54,7 +54,7 @@ 'Interaction', 'InteractionMessage', 'InteractionResponse', - 'InteractionCallback', + 'InteractionCallbackResponse', 'InteractionCallbackActivityInstance', ) @@ -659,7 +659,7 @@ def __init__(self, data: InteractionCallbackActivityPayload) -> None: self.id: str = data['id'] -class InteractionCallback(Generic[ClientT]): +class InteractionCallbackResponse(Generic[ClientT]): """Represents an interaction response callback. .. versionadded:: 2.5 @@ -776,7 +776,7 @@ async def defer( *, ephemeral: bool = False, thinking: bool = False, - ) -> Optional[InteractionCallback[ClientT]]: + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Defers the interaction response. @@ -791,7 +791,7 @@ async def defer( - :attr:`InteractionType.modal_submit` .. versionchanged:: 2.5 - This now returns a :class:`InteractionCallback` instance. + This now returns a :class:`InteractionCallbackResponse` instance. Parameters ----------- @@ -814,7 +814,7 @@ async def defer( Returns ------- - Optional[:class:`InteractionCallback`] + Optional[:class:`InteractionCallbackResponse`] The interaction callback resource, or ``None``. """ if self._response_type: @@ -849,7 +849,7 @@ async def defer( params=params, ) self._response_type = InteractionResponseType(defer_type) - return InteractionCallback( + return InteractionCallbackResponse( data=response, parent=self._parent, state=self._parent._state, @@ -904,13 +904,13 @@ async def send_message( silent: bool = False, delete_after: Optional[float] = None, poll: Poll = MISSING, - ) -> InteractionCallback[ClientT]: + ) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a message. .. versionchanged:: 2.5 - This now returns a :class:`InteractionCallback` instance. + This now returns a :class:`InteractionCallbackResponse` instance. Parameters ----------- @@ -968,7 +968,7 @@ async def send_message( Returns ------- - :class:`InteractionCallback` + :class:`InteractionCallbackResponse` The interaction callback data. """ if self._response_type: @@ -1031,7 +1031,7 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) - return InteractionCallback( + return InteractionCallbackResponse( data=response, parent=self._parent, state=self._parent._state, @@ -1049,14 +1049,14 @@ async def edit_message( allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, - ) -> Optional[InteractionCallback[ClientT]]: + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Responds to this interaction by editing the original message of a component or modal interaction. .. versionchanged:: 2.5 - This now returns a :class:`InteractionCallback` instance. + This now returns a :class:`InteractionCallbackResponse` instance. Parameters ----------- @@ -1106,7 +1106,7 @@ async def edit_message( Returns ------- - Optional[:class:`InteractionCallback`] + Optional[:class:`InteractionCallbackResponse`] The interaction callback data, or ``None`` if editing the message was not possible. """ if self._response_type: @@ -1175,20 +1175,20 @@ async def inner_call(delay: float = delete_after): asyncio.create_task(inner_call()) - return InteractionCallback( + return InteractionCallbackResponse( data=response, parent=self._parent, state=self._parent._state, type=self._response_type, ) - async def send_modal(self, modal: Modal, /) -> InteractionCallback[ClientT]: + async def send_modal(self, modal: Modal, /) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a modal. .. versionchanged:: 2.5 - This now returns a :class:`InteractionCallback` instance. + This now returns a :class:`InteractionCallbackResponse` instance. Parameters ----------- @@ -1208,7 +1208,7 @@ async def send_modal(self, modal: Modal, /) -> InteractionCallback[ClientT]: Returns ------- - :class:`InteractionCallback` + :class:`InteractionCallbackResponse` The interaction callback data. """ if self._response_type: @@ -1232,7 +1232,7 @@ async def send_modal(self, modal: Modal, /) -> InteractionCallback[ClientT]: self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal - return InteractionCallback( + return InteractionCallbackResponse( data=response, parent=self._parent, state=self._parent._state, diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 0bf69903bfc7..feab669073ea 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -28,12 +28,12 @@ InteractionResponse .. autoclass:: InteractionResponse() :members: -InteractionCallback -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +InteractionCallbackResponse +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attributetable:: InteractionCallback +.. attributetable:: InteractionCallbackResponse -.. autoclass:: InteractionCallback() +.. autoclass:: InteractionCallbackResponse() :members: InteractionCallbackActivityInstance From 8953938a53d8191b4c69c057e57911c0e31e8d2a Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 03:16:51 -0500 Subject: [PATCH 191/354] Update Pyright to v1.1.394 --- .github/workflows/lint.yml | 2 +- discord/abc.py | 7 +++++-- discord/activity.py | 10 +++++----- discord/app_commands/commands.py | 8 ++++---- discord/app_commands/transformers.py | 2 +- discord/app_commands/tree.py | 2 +- discord/components.py | 6 +++--- discord/enums.py | 12 ++++++------ discord/ext/commands/bot.py | 4 ++-- discord/ext/commands/context.py | 2 +- discord/ext/commands/converter.py | 6 +++--- discord/ext/commands/core.py | 2 +- discord/ext/commands/errors.py | 7 ++++--- discord/ext/commands/flags.py | 2 +- discord/ext/commands/hybrid.py | 13 +++++++------ discord/gateway.py | 4 ++-- discord/guild.py | 5 +++-- discord/interactions.py | 5 +++-- discord/invite.py | 2 +- discord/member.py | 6 +++--- discord/message.py | 23 ++++++++++++----------- discord/raw_models.py | 14 ++++++++------ discord/role.py | 2 +- discord/state.py | 8 ++++---- discord/threads.py | 2 +- discord/types/guild.py | 4 ++-- discord/ui/select.py | 5 ++++- discord/ui/view.py | 2 +- discord/utils.py | 6 +++--- discord/widget.py | 2 +- 30 files changed, 94 insertions(+), 81 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9e70f794faab..79b7ac8ec766 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,7 +38,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: - version: '1.1.351' + version: '1.1.394' warnings: false no-comments: ${{ matrix.python-version != '3.x' }} diff --git a/discord/abc.py b/discord/abc.py index 891404b33f4a..70531fb2005e 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -102,6 +102,9 @@ GuildChannel as GuildChannelPayload, OverwriteType, ) + from .types.guild import ( + ChannelPositionUpdate, + ) from .types.snowflake import ( SnowflakeList, ) @@ -1232,11 +1235,11 @@ async def move(self, **kwargs: Any) -> None: raise ValueError('Could not resolve appropriate move position') channels.insert(max((index + offset), 0), self) - payload = [] + payload: List[ChannelPositionUpdate] = [] lock_permissions = kwargs.get('sync_permissions', False) reason = kwargs.get('reason') for index, channel in enumerate(channels): - d = {'id': channel.id, 'position': index} + d: ChannelPositionUpdate = {'id': channel.id, 'position': index} if parent_id is not MISSING and channel.id == self.id: d.update(parent_id=parent_id, lock_permissions=lock_permissions) payload.append(d) diff --git a/discord/activity.py b/discord/activity.py index c692443f987a..324bea42f290 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -273,7 +273,7 @@ def to_dict(self) -> Dict[str, Any]: def start(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.""" try: - timestamp = self.timestamps['start'] / 1000 + timestamp = self.timestamps['start'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -283,7 +283,7 @@ def start(self) -> Optional[datetime.datetime]: def end(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.""" try: - timestamp = self.timestamps['end'] / 1000 + timestamp = self.timestamps['end'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -293,7 +293,7 @@ def end(self) -> Optional[datetime.datetime]: def large_image_url(self) -> Optional[str]: """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.""" try: - large_image = self.assets['large_image'] + large_image = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -303,7 +303,7 @@ def large_image_url(self) -> Optional[str]: def small_image_url(self) -> Optional[str]: """Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable.""" try: - small_image = self.assets['small_image'] + small_image = self.assets['small_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: @@ -525,7 +525,7 @@ def twitch_name(self) -> Optional[str]: """ try: - name = self.assets['large_image'] + name = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: return None else: diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index a872fb4be276..d5b8d93b27e5 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -903,7 +903,7 @@ async def _invoke_autocomplete(self, interaction: Interaction, name: str, namesp predicates = getattr(param.autocomplete, '__discord_app_commands_checks__', []) if predicates: try: - passed = await async_all(f(interaction) for f in predicates) + passed = await async_all(f(interaction) for f in predicates) # type: ignore except Exception: passed = False @@ -1014,7 +1014,7 @@ async def _check_can_run(self, interaction: Interaction) -> bool: if not predicates: return True - return await async_all(f(interaction) for f in predicates) + return await async_all(f(interaction) for f in predicates) # type: ignore def error(self, coro: Error[GroupT]) -> Error[GroupT]: """A decorator that registers a coroutine as a local error handler. @@ -1308,7 +1308,7 @@ async def _check_can_run(self, interaction: Interaction) -> bool: if not predicates: return True - return await async_all(f(interaction) for f in predicates) + return await async_all(f(interaction) for f in predicates) # type: ignore def _has_any_error_handlers(self) -> bool: return self.on_error is not None @@ -1842,7 +1842,7 @@ def error(self, coro: ErrorFunc) -> ErrorFunc: if len(params) != 2: raise TypeError('The error handler must have 2 parameters.') - self.on_error = coro + self.on_error = coro # type: ignore return coro async def interaction_check(self, interaction: Interaction, /) -> bool: diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index e7b001727343..c18485d8c0b4 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -235,7 +235,7 @@ def __call__(self) -> None: pass def __or__(self, rhs: Any) -> Any: - return Union[self, rhs] # type: ignore + return Union[self, rhs] @property def type(self) -> AppCommandOptionType: diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index 90b9a21ab958..3099071c01e0 100644 --- a/discord/app_commands/tree.py +++ b/discord/app_commands/tree.py @@ -859,7 +859,7 @@ def error(self, coro: ErrorFunc[ClientT]) -> ErrorFunc[ClientT]: if len(params) != 2: raise TypeError('error handler must have 2 parameters') - self.on_error = coro + self.on_error = coro # type: ignore return coro def command( diff --git a/discord/components.py b/discord/components.py index 2af2d6d20d8b..b3f978eb1bc4 100644 --- a/discord/components.py +++ b/discord/components.py @@ -196,12 +196,12 @@ def __init__(self, data: ButtonComponentPayload, /) -> None: self.label: Optional[str] = data.get('label') self.emoji: Optional[PartialEmoji] try: - self.emoji = PartialEmoji.from_dict(data['emoji']) + self.emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.emoji = None try: - self.sku_id: Optional[int] = int(data['sku_id']) + self.sku_id: Optional[int] = int(data['sku_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.sku_id = None @@ -415,7 +415,7 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: @classmethod def from_dict(cls, data: SelectOptionPayload) -> SelectOption: try: - emoji = PartialEmoji.from_dict(data['emoji']) + emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: emoji = None diff --git a/discord/enums.py b/discord/enums.py index ce772cc87285..7915bcb4b04b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -84,13 +84,13 @@ def _create_value_cls(name: str, comparable: bool): # All the type ignores here are due to the type checker being unable to recognise # Runtime type creation without exploding. cls = namedtuple('_EnumValue_' + name, 'name value') - cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' # type: ignore - cls.__str__ = lambda self: f'{name}.{self.name}' # type: ignore + cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' + cls.__str__ = lambda self: f'{name}.{self.name}' if comparable: - cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value # type: ignore - cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value # type: ignore - cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value # type: ignore - cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value # type: ignore + cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value + cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value + cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value + cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value return cls diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 208948335568..8ce872f1af7a 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -172,7 +172,7 @@ def __init__( **options: Any, ) -> None: super().__init__(intents=intents, **options) - self.command_prefix: PrefixType[BotT] = command_prefix + self.command_prefix: PrefixType[BotT] = command_prefix # type: ignore self.extra_events: Dict[str, List[CoroFunc]] = {} # Self doesn't have the ClientT bound, but since this is a mixin it technically does self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore @@ -487,7 +487,7 @@ async def can_run(self, ctx: Context[BotT], /, *, call_once: bool = False) -> bo if len(data) == 0: return True - return await discord.utils.async_all(f(ctx) for f in data) + return await discord.utils.async_all(f(ctx) for f in data) # type: ignore async def is_owner(self, user: User, /) -> bool: """|coro| diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index fa89b5078503..93303973523a 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -82,7 +82,7 @@ def is_cog(obj: Any) -> TypeGuard[Cog]: return hasattr(obj, '__cog_commands__') -class DeferTyping: +class DeferTyping(Generic[BotT]): def __init__(self, ctx: Context[BotT], *, ephemeral: bool): self.ctx: Context[BotT] = ctx self.ephemeral: bool = ephemeral diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 744a00fd3020..d316f6ccc63c 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1125,7 +1125,7 @@ def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]: args = getattr(converter, '__args__', ()) if discord.utils.PY_310 and converter.__class__ is types.UnionType: # type: ignore - converter = Union[args] # type: ignore + converter = Union[args] origin = getattr(converter, '__origin__', None) @@ -1138,7 +1138,7 @@ def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]: if origin is Union and type(None) in args: raise TypeError(f'Greedy[{converter!r}] is invalid.') - return cls(converter=converter) + return cls(converter=converter) # type: ignore @property def constructed_converter(self) -> Any: @@ -1325,7 +1325,7 @@ async def _actual_conversion(ctx: Context[BotT], converter: Any, argument: str, else: return await converter().convert(ctx, argument) elif isinstance(converter, Converter): - return await converter.convert(ctx, argument) # type: ignore + return await converter.convert(ctx, argument) except CommandError: raise except Exception as exc: diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 1c682a957725..372fcbedfdf6 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -1285,7 +1285,7 @@ async def can_run(self, ctx: Context[BotT], /) -> bool: # since we have no checks, then we just return True. return True - return await discord.utils.async_all(predicate(ctx) for predicate in predicates) + return await discord.utils.async_all(predicate(ctx) for predicate in predicates) # type: ignore finally: ctx.command = original diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index f81d54b4d89c..feb4aee279cd 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -24,18 +24,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union, Generic from discord.errors import ClientException, DiscordException from discord.utils import _human_join +from ._types import BotT + if TYPE_CHECKING: from discord.abc import GuildChannel from discord.threads import Thread from discord.types.snowflake import Snowflake, SnowflakeList from discord.app_commands import AppCommandError - from ._types import BotT from .context import Context from .converter import Converter from .cooldowns import BucketType, Cooldown @@ -235,7 +236,7 @@ class CheckFailure(CommandError): pass -class CheckAnyFailure(CheckFailure): +class CheckAnyFailure(Generic[BotT], CheckFailure): """Exception raised when all predicates in :func:`check_any` fail. This inherits from :exc:`CheckFailure`. diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 8afd29a3d259..0766ecae34ea 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -443,7 +443,7 @@ async def convert_flag(ctx: Context[BotT], argument: str, flag: Flag, annotation return await convert_flag(ctx, argument, flag, annotation) elif origin is Union and type(None) in annotation.__args__: # typing.Optional[x] - annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))] # type: ignore + annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))] return await run_converters(ctx, annotation, argument, param) elif origin is dict: # typing.Dict[K, V] -> typing.Tuple[K, V] diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index af9e63a7b383..0857003fad90 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -203,9 +203,9 @@ def replace_parameter( # Fallback to see if the behaviour needs changing origin = getattr(converter, '__origin__', None) args = getattr(converter, '__args__', []) - if isinstance(converter, Range): + if isinstance(converter, Range): # type: ignore # Range is not an Annotation at runtime r = converter - param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) + param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) # type: ignore elif isinstance(converter, Greedy): # Greedy is "optional" in ext.commands # However, in here, it probably makes sense to make it required. @@ -257,7 +257,7 @@ def replace_parameter( inner = args[0] is_inner_transformer = is_transformer(inner) if is_converter(inner) and not is_inner_transformer: - param = param.replace(annotation=Optional[ConverterTransformer(inner, original)]) # type: ignore + param = param.replace(annotation=Optional[ConverterTransformer(inner, original)]) else: raise elif origin: @@ -424,10 +424,10 @@ async def _check_can_run(self, interaction: discord.Interaction) -> bool: if not ret: return False - if self.checks and not await async_all(f(interaction) for f in self.checks): + if self.checks and not await async_all(f(interaction) for f in self.checks): # type: ignore return False - if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): + if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): # type: ignore return False return True @@ -915,7 +915,8 @@ def hybrid_command( def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridCommand[CogT, P, T]: if isinstance(func, Command): raise TypeError('Callback is already a command.') - return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) + # Pyright does not allow Command[Any] to be assigned to Command[CogT] despite it being okay here + return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore return decorator diff --git a/discord/gateway.py b/discord/gateway.py index d15c617d14c0..44656df03633 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -831,7 +831,7 @@ def __init__( self._close_code: Optional[int] = None self.secret_key: Optional[List[int]] = None if hook: - self._hook = hook + self._hook = hook # type: ignore async def _hook(self, *args: Any) -> None: pass @@ -893,7 +893,7 @@ async def from_connection_state( return ws - async def select_protocol(self, ip: str, port: int, mode: int) -> None: + async def select_protocol(self, ip: str, port: int, mode: str) -> None: payload = { 'op': self.SELECT_PROTOCOL, 'd': { diff --git a/discord/guild.py b/discord/guild.py index b7e53f0c79ba..20a50d4e932f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -551,7 +551,8 @@ def _update_voice_state(self, data: GuildVoiceState, channel_id: int) -> Tuple[O member = self.get_member(user_id) if member is None: try: - member = Member(data=data['member'], state=self._state, guild=self) + member_data = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] + member = Member(data=member_data, state=self._state, guild=self) except KeyError: member = None @@ -573,7 +574,7 @@ def _create_unavailable(cls, *, state: ConnectionState, guild_id: int, data: Opt def _from_data(self, guild: GuildPayload) -> None: try: - self._member_count = guild['member_count'] + self._member_count = guild['member_count'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass diff --git a/discord/interactions.py b/discord/interactions.py index 11cb9792988b..b62c01580a82 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -219,14 +219,15 @@ def _from_data(self, data: InteractionPayload): int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items() } try: - self.context = AppCommandContext._from_value([data['context']]) + value = data['context'] # pyright: ignore[reportTypedDictNotRequiredAccess] + self.context = AppCommandContext._from_value([value]) except KeyError: self.context = AppCommandContext() self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US')) self.guild_locale: Optional[Locale] try: - self.guild_locale = try_enum(Locale, data['guild_locale']) + self.guild_locale = try_enum(Locale, data['guild_locale']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_locale = None diff --git a/discord/invite.py b/discord/invite.py index 1d8dd1c8ef73..dd8cc954ac53 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -437,7 +437,7 @@ def __init__( def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self: guild: Optional[Union[Guild, PartialInviteGuild]] try: - guild_data = data['guild'] + guild_data = data['guild'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: # If we're here, then this is a group DM guild = None diff --git a/discord/member.py b/discord/member.py index 8dee3354608a..6af1571f4d34 100644 --- a/discord/member.py +++ b/discord/member.py @@ -326,7 +326,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self._flags: int = data['flags'] self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') try: - self._permissions = int(data['permissions']) + self._permissions = int(data['permissions']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self._permissions = None @@ -418,12 +418,12 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: # the nickname change is optional, # if it isn't in the payload then it didn't change try: - self.nick = data['nick'] + self.nick = data['nick'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass try: - self.pending = data['pending'] + self.pending = data['pending'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass diff --git a/discord/message.py b/discord/message.py index 3016d2f2945c..44431b595eea 100644 --- a/discord/message.py +++ b/discord/message.py @@ -773,7 +773,7 @@ def __init__(self, *, state: ConnectionState, guild: Optional[Guild], data: Mess self.user: Union[User, Member] = MISSING try: - payload = data['member'] + payload = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.user = state.create_user(data['user']) else: @@ -2200,7 +2200,8 @@ def __init__( self.poll: Optional[Poll] = None try: - self.poll = Poll._from_data(data=data['poll'], message=self, state=state) + poll = data['poll'] # pyright: ignore[reportTypedDictNotRequiredAccess] + self.poll = Poll._from_data(data=poll, message=self, state=state) except KeyError: pass @@ -2214,7 +2215,7 @@ def __init__( if self.guild is not None: try: - thread = data['thread'] + thread = data['thread'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2229,7 +2230,7 @@ def __init__( # deprecated try: - interaction = data['interaction'] + interaction = data['interaction'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2237,20 +2238,20 @@ def __init__( self.interaction_metadata: Optional[MessageInteractionMetadata] = None try: - interaction_metadata = data['interaction_metadata'] + interaction_metadata = data['interaction_metadata'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: self.interaction_metadata = MessageInteractionMetadata(state=state, guild=self.guild, data=interaction_metadata) try: - ref = data['message_reference'] + ref = data['message_reference'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.reference = None else: self.reference = ref = MessageReference.with_state(state, ref) try: - resolved = data['referenced_message'] + resolved = data['referenced_message'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2277,7 +2278,7 @@ def __init__( self.application: Optional[MessageApplication] = None try: - application = data['application'] + application = data['application'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2285,7 +2286,7 @@ def __init__( self.role_subscription: Optional[RoleSubscriptionInfo] = None try: - role_subscription = data['role_subscription_data'] + role_subscription = data['role_subscription_data'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2293,7 +2294,7 @@ def __init__( self.purchase_notification: Optional[PurchaseNotification] = None try: - purchase_notification = data['purchase_notification'] + purchase_notification = data['purchase_notification'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2301,7 +2302,7 @@ def __init__( for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: - getattr(self, f'_handle_{handler}')(data[handler]) + getattr(self, f'_handle_{handler}')(data[handler]) # type: ignore except KeyError: continue diff --git a/discord/raw_models.py b/discord/raw_models.py index c8c8b0e388ef..8304559a1ef0 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -104,7 +104,7 @@ def __init__(self, data: MessageDeleteEvent) -> None: self.channel_id: int = int(data['channel_id']) self.cached_message: Optional[Message] = None try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -132,7 +132,7 @@ def __init__(self, data: BulkMessageDeleteEvent) -> None: self.cached_messages: List[Message] = [] try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -248,7 +248,7 @@ def __init__(self, data: ReactionActionEvent, emoji: PartialEmoji, event_type: R self.type: ReactionType = try_enum(ReactionType, data['type']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -281,7 +281,7 @@ def __init__(self, data: ReactionClearEvent) -> None: self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -311,7 +311,7 @@ def __init__(self, data: ReactionClearEmojiEvent, emoji: PartialEmoji) -> None: self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -338,7 +338,9 @@ def __init__(self, data: IntegrationDeleteEvent) -> None: self.guild_id: int = int(data['guild_id']) try: - self.application_id: Optional[int] = int(data['application_id']) + self.application_id: Optional[int] = int( + data['application_id'] # pyright: ignore[reportTypedDictNotRequiredAccess] + ) except KeyError: self.application_id: Optional[int] = None diff --git a/discord/role.py b/discord/role.py index 571ab9f20bbf..d7fe1e08bbe2 100644 --- a/discord/role.py +++ b/discord/role.py @@ -286,7 +286,7 @@ def _update(self, data: RolePayload): self._flags: int = data.get('flags', 0) try: - self.tags = RoleTags(data['tags']) + self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.tags = None diff --git a/discord/state.py b/discord/state.py index b1409f809100..c4b71b368ad3 100644 --- a/discord/state.py +++ b/discord/state.py @@ -540,7 +540,7 @@ def _get_guild_channel( ) -> Tuple[Union[Channel, Thread], Optional[Guild]]: channel_id = int(data['channel_id']) try: - guild_id = guild_id or int(data['guild_id']) + guild_id = guild_id or int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] guild = self._get_guild(guild_id) except KeyError: channel = DMChannel._from_message(self, channel_id) @@ -736,7 +736,7 @@ def parse_message_update(self, data: gw.MessageUpdateEvent) -> None: if 'components' in data: try: - entity_id = int(data['interaction']['id']) + entity_id = int(data['interaction']['id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except (KeyError, ValueError): entity_id = raw.message_id @@ -935,7 +935,7 @@ def parse_channel_create(self, data: gw.ChannelCreateEvent) -> None: def parse_channel_pins_update(self, data: gw.ChannelPinsUpdateEvent) -> None: channel_id = int(data['channel_id']) try: - guild = self._get_guild(int(data['guild_id'])) + guild = self._get_guild(int(data['guild_id'])) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: guild = None channel = self._get_private_channel(channel_id) @@ -1017,7 +1017,7 @@ def parse_thread_list_sync(self, data: gw.ThreadListSyncEvent) -> None: return try: - channel_ids = {int(i) for i in data['channel_ids']} + channel_ids = {int(i) for i in data['channel_ids']} # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: # If not provided, then the entire guild is being synced # So all previous thread data should be overwritten diff --git a/discord/threads.py b/discord/threads.py index 27288693457b..024b22506b04 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -192,7 +192,7 @@ def _from_data(self, data: ThreadPayload): self.me: Optional[ThreadMember] try: - member = data['member'] + member = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.me = None else: diff --git a/discord/types/guild.py b/discord/types/guild.py index e0a1f3e54438..7ac90b89ea53 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -179,8 +179,8 @@ class GuildMFALevel(TypedDict): class ChannelPositionUpdate(TypedDict): id: Snowflake position: Optional[int] - lock_permissions: Optional[bool] - parent_id: Optional[Snowflake] + lock_permissions: NotRequired[Optional[bool]] + parent_id: NotRequired[Optional[Snowflake]] class _RolePositionRequired(TypedDict): diff --git a/discord/ui/select.py b/discord/ui/select.py index 6738b9727c56..1ef085cc5df2 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import ( Any, @@ -330,7 +331,9 @@ def _refresh_state(self, interaction: Interaction, data: SelectMessageComponentI values = selected_values.get({}) payload: List[PossibleValue] try: - resolved = Namespace._get_resolved_items(interaction, data['resolved']) + resolved = Namespace._get_resolved_items( + interaction, data['resolved'] # pyright: ignore[reportTypedDictNotRequiredAccess] + ) payload = list(resolved.values()) except KeyError: payload = data.get("values", []) # type: ignore diff --git a/discord/ui/view.py b/discord/ui/view.py index ad5ea058536f..dd44944ec0ef 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -177,7 +177,7 @@ def _init_children(self) -> List[Item[Self]]: children = [] for func in self.__view_children_items__: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) + item.callback = _ViewCallback(func, self, item) # type: ignore item._view = self setattr(self, func.__name__, item) children.append(item) diff --git a/discord/utils.py b/discord/utils.py index 1caffe7f57f0..bcdf922b402b 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -714,13 +714,13 @@ async def maybe_coroutine(f: MaybeAwaitableFunc[P, T], *args: P.args, **kwargs: if _isawaitable(value): return await value else: - return value # type: ignore + return value async def async_all( gen: Iterable[Union[T, Awaitable[T]]], *, - check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, + check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, # type: ignore ) -> bool: for elem in gen: if check(elem): @@ -1121,7 +1121,7 @@ def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: literal_cls = type(Literal[0]) for p in parameters: if isinstance(p, literal_cls): - params.extend(p.__args__) + params.extend(p.__args__) # type: ignore else: params.append(p) return tuple(params) diff --git a/discord/widget.py b/discord/widget.py index 8220086652d8..cdb883fd96db 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -184,7 +184,7 @@ def __init__( self.suppress: Optional[bool] = data.get('suppress', False) try: - game = data['game'] + game = data['game'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: activity = None else: From 43e1c55d1124fd848787ed25546f80757d788177 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 04:24:01 -0500 Subject: [PATCH 192/354] Remove with_response parameter documentation --- discord/interactions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index b62c01580a82..b9d9a4d11ea7 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -1195,10 +1195,6 @@ async def send_modal(self, modal: Modal, /) -> InteractionCallbackResponse[Clien ----------- modal: :class:`~discord.ui.Modal` The modal to send. - with_response: :class:`bool` - Whether to return the interaction response callback resource. - - .. versionadded:: 2.5 Raises ------- From e8d571b194628220cc4f21752b81f228f43f7f73 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 07:28:12 -0500 Subject: [PATCH 193/354] Add missing Poll attribute documentation --- discord/poll.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/discord/poll.py b/discord/poll.py index 767f8ffae82a..6ab680abd26e 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -336,6 +336,15 @@ class Poll: Defaults to ``False``. layout_type: :class:`PollLayoutType` The layout type of the poll. Defaults to :attr:`PollLayoutType.default`. + + Attributes + ----------- + duration: :class:`datetime.timedelta` + The duration of the poll. + multiple: :class:`bool` + Whether users are allowed to select more than one answer. + layout_type: :class:`PollLayoutType` + The layout type of the poll. """ __slots__ = ( From 3ad0662f688da154c0a2ee24f46d505177256002 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 07:28:25 -0500 Subject: [PATCH 194/354] Document SoundboardSoundConverter --- docs/ext/commands/api.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index f55225614215..3da5cae163f8 100644 --- a/docs/ext/commands/api.rst +++ b/docs/ext/commands/api.rst @@ -531,6 +531,11 @@ Converters .. autoclass:: discord.ext.commands.ScheduledEventConverter :members: +.. attributetable:: discord.ext.commands.SoundboardSoundConverter + +.. autoclass:: discord.ext.commands.SoundboardSoundConverter + :members: + .. attributetable:: discord.ext.commands.clean_content .. autoclass:: discord.ext.commands.clean_content From a1aa59706c6207e114d0758e0faa496db214628d Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 07:56:37 -0500 Subject: [PATCH 195/354] Add changelog for v2.5 --- docs/whats_new.rst | 154 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index d51de610b90a..d3763a588e0f 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,160 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p5p0: + +v2.5.0 +------- + +New Features +~~~~~~~~~~~~~ + +- Add support for message forwarding (:issue:`9950`) + - Adds :class:`MessageReferenceType` + - Adds :class:`MessageSnapshot` + - Adds ``type`` parameter to :class:`MessageReference`, :meth:`MessageReference.from_message`, and :meth:`PartialMessage.to_reference` + - Add :meth:`PartialMessage.forward` + +- Add SKU subscriptions support (:issue:`9930`) + - Adds new events :func:`on_subscription_create`, :func:`on_subscription_update`, and :func:`on_subscription_delete` + - Add :class:`SubscriptionStatus` enum + - Add :class:`Subscription` model + - Add :meth:`SKU.fetch_subscription` and :meth:`SKU.subscriptions` + +- Add support for application emojis (:issue:`9891`) + - Add :meth:`Client.create_application_emoji` + - Add :meth:`Client.fetch_application_emoji` + - Add :meth:`Client.fetch_application_emojis` + - Add :meth:`Emoji.is_application_owned` + +- Support for Soundboard and VC effects (:issue:`9349`) + - Add :class:`BaseSoundboardSound`, :class:`SoundboardDefaultSound`, and :class:`SoundboardSound` + - Add :class:`VoiceChannelEffect` + - Add :class:`VoiceChannelEffectAnimation` + - Add :class:`VoiceChannelEffectAnimationType` + - Add :class:`VoiceChannelSoundEffect` + - Add :meth:`VoiceChannel.send_sound` + - Add new audit log actions: :attr:`AuditLogAction.soundboard_sound_create`, :attr:`AuditLogAction.soundboard_sound_update`, and :attr:`AuditLogAction.soundboard_sound_delete`. + - Add :attr:`Intents.expressions` and make :attr:`Intents.emojis` and :attr:`Intents.emojis_and_stickers` aliases of that intent. + - Add new events: :func:`on_soundboard_sound_create`, :func:`on_soundboard_sound_update`, :func:`on_soundboard_sound_delete`, and :func:`on_voice_channel_effect`. + - Add methods and properties dealing with soundboards: + - :attr:`Client.soundboard_sounds` + - :attr:`Guild.soundboard_sounds` + - :meth:`Client.get_soundboard_sound` + - :meth:`Guild.get_soundboard_sound` + - :meth:`Client.fetch_soundboard_default_sounds` + - :meth:`Guild.fetch_soundboard_sound` + - :meth:`Guild.fetch_soundboard_sounds` + - :meth:`Guild.create_soundboard_sound` + +- Add support for retrieving interaction responses when sending a response (:issue:`9957`) + - Methods from :class:`InteractionResponse` now return :class:`InteractionCallbackResponse` + - Depending on the interaction response type, :attr:`InteractionCallbackResponse.resource` will be different + +- Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`) +- Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`) +- Add +- Add richer :meth:`Role.move` interface (:issue:`10100`) +- Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`) +- Add new flags for :class:`AttachmentFlags` (:issue:`10085`) +- Add :func:`on_raw_presence_update` event that does not depend on cache state (:issue:`10048`) + - This requires setting the ``enable_raw_presences`` keyword argument within :class:`Client`. + +- Add :attr:`ForumChannel.members` property. (:issue:`10034`) +- Add ``exclude_deleted`` parameter to :meth:`Client.entitlements` (:issue:`10027`) +- Add :meth:`Client.fetch_guild_preview` (:issue:`9986`) +- Add :meth:`AutoShardedClient.fetch_session_start_limits` (:issue:`10007`) +- Add :attr:`PartialMessageable.mention` (:issue:`9988`) +- Add command target to :class:`MessageInteractionMetadata` (:issue:`10004`) + - :attr:`MessageInteractionMetadata.target_user` + - :attr:`MessageInteractionMetadata.target_message_id` + - :attr:`MessageInteractionMetadata.target_message` + +- Add :attr:`Message.forward` flag (:issue:`9978`) + +- Add support for purchase notification messages (:issue:`9906`) + - Add new type :attr:`MessageType.purchase_notification` + - Add new models :class:`GuildProductPurchase` and :class:`PurchaseNotification` + - Add :attr:`Message.purchase_notification` + +- Add ``category`` parameter to :meth:`.abc.GuildChannel.clone` (:issue:`9941`) +- Add support for message call (:issue:`9911`) + - Add new models :class:`CallMessage` + - Add :attr:`Message.call` attribute + +- Parse full message for message edit event (:issue:`10035`) + - Adds :attr:`RawMessageUpdateEvent.message` attribute + - Potentially speeds up :func:`on_message_edit` by no longer copying data + +- Add support for retrieving and editing integration type configuration (:issue:`9818`) + - This adds :class:`IntegrationTypeConfig` + - Retrievable via :attr:`AppInfo.guild_integration_config` and :attr:`AppInfo.user_integration_config`. + - Editable via :meth:`AppInfo.edit` + +- Allow passing ``None`` for ``scopes`` parameter in :func:`utils.oauth_url` (:issue:`10078`) +- Add support for :attr:`MessageType.poll_result` messages (:issue:`9905`) +- Add various new :class:`MessageFlags` +- Add :meth:`Member.fetch_voice` (:issue:`9908`) +- Add :attr:`Guild.dm_spam_detected_at` and :meth:`Guild.is_dm_spam_detected` (:issue:`9808`) +- Add :attr:`Guild.raid_detected_at` and :meth:`Guild.is_raid_detected` (:issue:`9808`) +- Add :meth:`Client.fetch_premium_sticker_pack` (:issue:`9909`) +- Add :attr:`AppInfo.approximate_user_install_count` (:issue:`9915`) +- Add :meth:`Guild.fetch_role` (:issue:`9921`) +- Add :attr:`Attachment.title` (:issue:`9904`) +- Add :attr:`Member.guild_banner` and :attr:`Member.display_banner` +- Re-add ``connector`` parameter that was removed during v2.0 (:issue:`9900`) +- |commands| Add :class:`~discord.ext.commands.SoundboardSoundConverter` (:issue:`9973`) + +Bug Fixes +~~~~~~~~~~ + +- Change the default file size limit for :attr:`Guild.filesize_limit` to match new Discord limit of 10 MiB (:issue:`10084`) +- Handle improper 1000 close code closures by Discord + - This fixes an issue causing excessive IDENTIFY in large bots + +- Fix potential performance regression when dealing with cookies in the library owned session (:issue:`9916`) +- Add support for AEAD XChaCha20 Poly1305 encryption mode (:issue:`9953`) + - This allows voice to continue working when the older encryption modes eventually get removed. + - Support for DAVE is still tentative. + +- Fix large performance regression due to polls when creating messages +- Fix cases where :attr:`Member.roles` contains a ``None`` role (:issue:`10093`) +- Update all channel clone implementations to work as expected (:issue:`9935`) +- Fix bug in :meth:`Client.entitlements` only returning 100 entries (:issue:`10051`) +- Fix :meth:`TextChannel.clone` always sending slowmode when not applicable to news channels (:issue:`9967`) +- Fix :attr:`Message.system_content` for :attr:`MessageType.role_subscription_purchase` renewals (:issue:`9955`) +- Fix :attr:`Sticker.url` for GIF stickers (:issue:`9913`) +- Fix :attr:`User.default_avatar` for team users and webhooks (:issue:`9907`) +- Fix potential rounding error in :attr:`Poll.duration` (:issue:`9903`) +- Fix introduced potential TypeError when raising :exc:`app_commands.CommandSyncFailure` +- Fix :attr:`AuditLogEntry.target` causing errors for :attr:`AuditLogAction.message_pin` and :attr:`AuditLogAction.message_unpin` actions (:issue:`10061`). +- Fix incorrect :class:`ui.Select` maximum option check (:issue:`9878`, :issue:`9879`) +- Fix path sanitisation for absolute Windows paths when using ``__main__`` (:issue:`10096`, :issue:`10097`) +- |tasks| Fix race condition when setting timer handle when using uvloop (:issue:`10020`) +- |commands| Fix issue with category cooldowns outside of guild channels (:issue:`9959`) +- |commands| Fix :meth:`Context.defer ` unconditionally deferring +- |commands| Fix callable FlagConverter defaults on hybrid commands not being called (:issue:`10037`) +- |commands| Unwrap :class:`~discord.ext.commands.Parameter` if given as default to :func:`~ext.commands.parameter` (:issue:`9977`) +- |commands| Fix fallback behaviour not being respected when calling replace for :class:`~.ext.commands.Parameter` (:issue:`10076`, :issue:`10077`) +- |commands| Respect ``enabled`` keyword argument for hybrid app commands (:issue:`10001`) + +Miscellaneous +~~~~~~~~~~~~~~ + +- Use a fallback package for ``audioop`` to allow the library to work in Python 3.13 or newer. +- Remove ``aiodns`` from being used on Windows (:issue:`9898`) +- Add zstd gateway compression to ``speed`` extras (:issue:`9947`) + - This can be installed using ``discord.py[speed]`` + +- Add proxy support fetching from the CDN (:issue:`9966`) +- Remove ``/`` from being safe from URI encoding when constructing paths internally +- Sanitize invite argument before calling the invite info endpoint +- Avoid returning in finally in specific places to prevent exception swallowing (:issue:`9981`, :issue:`9984`) +- Enforce and create random nonces when creating messages throughout the library +- Revert IPv6 block in the library (:issue:`9870`) +- Allow passing :class:`Permissions` object to :func:`app_commands.default_permissions` decorator (:issue:`9951`, :issue:`9971`) + + .. _vp2p4p0: v2.4.0 From e837ac1cacac3c211ed500c4ef76624bceb4521e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 08:05:13 -0500 Subject: [PATCH 196/354] Version bump to v2.5.0 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index f850ee4acbea..203f986c1d0b 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.5.0a' +__version__ = '2.5.0' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 2cf1babb4ac40d6087ae94cdbad304e75ef090df Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 18 Feb 2025 08:06:01 -0500 Subject: [PATCH 197/354] Version bump for development --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 203f986c1d0b..48fe1092541e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.5.0' +__version__ = '2.6.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From ae2410fa3ace0628cd600918539406c6ef9df486 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:52:40 +0100 Subject: [PATCH 198/354] feat: Components V2 --- discord/attachment.py | 417 ++++++++++++++++++++++++++++++++++++ discord/components.py | 176 +++++++++++++-- discord/enums.py | 21 ++ discord/flags.py | 8 + discord/message.py | 298 +------------------------- discord/types/attachment.py | 58 +++++ discord/types/components.py | 69 +++++- discord/types/message.py | 26 +-- discord/ui/section.py | 50 +++++ 9 files changed, 781 insertions(+), 342 deletions(-) create mode 100644 discord/attachment.py create mode 100644 discord/types/attachment.py create mode 100644 discord/ui/section.py diff --git a/discord/attachment.py b/discord/attachment.py new file mode 100644 index 000000000000..2be4eac1aa73 --- /dev/null +++ b/discord/attachment.py @@ -0,0 +1,417 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the 'Software'), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +import io +from os import PathLike +from typing import TYPE_CHECKING, Any, Optional, Union + +from .mixins import Hashable +from .file import File +from .state import ConnectionState +from .flags import AttachmentFlags +from . import utils + +if TYPE_CHECKING: + from .types.attachment import Attachment as AttachmentPayload + +MISSING = utils.MISSING + +__all__ = ( + 'Attachment', + 'UnfurledAttachment', +) + + +class AttachmentBase: + url: str + + async def save( + self, + fp: Union[io.BufferedIOBase, PathLike[Any]], + *, + seek_begin: bool = True, + use_cached: bool = False, + ) -> int: + """|coro| + + Saves this attachment into a file-like object. + + Parameters + ---------- + fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`] + The file-like object to save this attachment to or the filename + to use. If a filename is passed then a file is created with that + filename and used instead. + seek_begin: :class:`bool` + Whether to seek to the beginning of the file after saving is + successfully done. + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + Raises + -------- + HTTPException + Saving the attachment failed. + NotFound + The attachment was deleted. + + Returns + -------- + :class:`int` + The number of bytes written. + """ + data = await self.read(use_cached=use_cached) + if isinstance(fp, io.BufferedIOBase): + written = fp.write(data) + if seek_begin: + fp.seek(0) + return written + else: + with open(fp, 'wb') as f: + return f.write(data) + + async def read(self, *, use_cached: bool = False) -> bytes: + """|coro| + + Retrieves the content of this attachment as a :class:`bytes` object. + + .. versionadded:: 1.1 + + Parameters + ----------- + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + Raises + ------ + HTTPException + Downloading the attachment failed. + Forbidden + You do not have permissions to access this attachment + NotFound + The attachment was deleted. + + Returns + ------- + :class:`bytes` + The contents of the attachment. + """ + url = self.proxy_url if use_cached else self.url + data = await self._http.get_from_cdn(url) + return data + + async def to_file( + self, + *, + filename: Optional[str] = MISSING, + description: Optional[str] = MISSING, + use_cached: bool = False, + spoiler: bool = False, + ) -> File: + """|coro| + + Converts the attachment into a :class:`File` suitable for sending via + :meth:`abc.Messageable.send`. + + .. versionadded:: 1.3 + + Parameters + ----------- + filename: Optional[:class:`str`] + The filename to use for the file. If not specified then the filename + of the attachment is used instead. + + .. versionadded:: 2.0 + description: Optional[:class:`str`] + The description to use for the file. If not specified then the + description of the attachment is used instead. + + .. versionadded:: 2.0 + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + .. versionadded:: 1.4 + spoiler: :class:`bool` + Whether the file is a spoiler. + + .. versionadded:: 1.4 + + Raises + ------ + HTTPException + Downloading the attachment failed. + Forbidden + You do not have permissions to access this attachment + NotFound + The attachment was deleted. + + Returns + ------- + :class:`File` + The attachment as a file suitable for sending. + """ + + data = await self.read(use_cached=use_cached) + file_filename = filename if filename is not MISSING else self.filename + file_description = ( + description if description is not MISSING else self.description + ) + return File( + io.BytesIO(data), + filename=file_filename, + description=file_description, + spoiler=spoiler, + ) + + +class Attachment(Hashable, AttachmentBase): + """Represents an attachment from Discord. + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the attachment. + + .. describe:: x == y + + Checks if the attachment is equal to another attachment. + + .. describe:: x != y + + Checks if the attachment is not equal to another attachment. + + .. describe:: hash(x) + + Returns the hash of the attachment. + + .. versionchanged:: 1.7 + Attachment can now be casted to :class:`str` and is hashable. + + Attributes + ------------ + id: :class:`int` + The attachment ID. + size: :class:`int` + The attachment size in bytes. + height: Optional[:class:`int`] + The attachment's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The attachment's width, in pixels. Only applicable to images and videos. + filename: :class:`str` + The attachment's filename. + url: :class:`str` + The attachment URL. If the message this attachment was attached + to is deleted, then this will 404. + proxy_url: :class:`str` + The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the + case of images. When the message is deleted, this URL might be valid for a few + minutes or not valid at all. + content_type: Optional[:class:`str`] + The attachment's `media type `_ + + .. versionadded:: 1.7 + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + + .. versionadded:: 2.0 + ephemeral: :class:`bool` + Whether the attachment is ephemeral. + + .. versionadded:: 2.0 + duration: Optional[:class:`float`] + The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + waveform: Optional[:class:`bytes`] + The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 + """ + + __slots__ = ( + 'id', + 'size', + 'height', + 'width', + 'filename', + 'url', + 'proxy_url', + '_http', + 'content_type', + 'description', + 'ephemeral', + 'duration', + 'waveform', + '_flags', + 'title', + ) + + def __init__(self, *, data: AttachmentPayload, state: ConnectionState): + self.id: int = int(data['id']) + self.size: int = data['size'] + self.height: Optional[int] = data.get('height') + self.width: Optional[int] = data.get('width') + self.filename: str = data['filename'] + self.url: str = data['url'] + self.proxy_url: str = data['proxy_url'] + self._http = state.http + self.content_type: Optional[str] = data.get('content_type') + self.description: Optional[str] = data.get('description') + self.ephemeral: bool = data.get('ephemeral', False) + self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') + + waveform = data.get('waveform') + self.waveform: Optional[bytes] = ( + utils._base64_to_bytes(waveform) if waveform is not None else None + ) + + self._flags: int = data.get('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: The attachment's flags.""" + return AttachmentFlags._from_value(self._flags) + + def is_spoiler(self) -> bool: + """:class:`bool`: Whether this attachment contains a spoiler.""" + return self.filename.startswith('SPOILER_') + + def is_voice_message(self) -> bool: + """:class:`bool`: Whether this attachment is a voice message.""" + return self.duration is not None and 'voice-message' in self.url + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + return self.url or '' + + def to_dict(self) -> AttachmentPayload: + result: AttachmentPayload = { + 'filename': self.filename, + 'id': self.id, + 'proxy_url': self.proxy_url, + 'size': self.size, + 'url': self.url, + 'spoiler': self.is_spoiler(), + } + if self.height: + result['height'] = self.height + if self.width: + result['width'] = self.width + if self.content_type: + result['content_type'] = self.content_type + if self.description is not None: + result['description'] = self.description + return result + + +class UnfurledAttachment(AttachmentBase): + """Represents an unfurled attachment item from a :class:`Component`. + + .. versionadded:: tbd + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the attachment. + + .. describe:: x == y + + Checks if the unfurled attachment is equal to another unfurled attachment. + + .. describe:: x != y + + Checks if the unfurled attachment is not equal to another unfurled attachment. + + Attributes + ---------- + url: :class:`str` + The unfurled attachment URL. + proxy_url: Optional[:class:`str`] + The proxy URL. This is cached version of the :attr:`~UnfurledAttachment.url` in the + case of images. When the message is deleted, this URL might be valid for a few + minutes or not valid at all. + + .. note:: + + This will be ``None`` if :meth:`.is_resolved` is ``False``. + height: Optional[:class:`int`] + The unfurled attachment's height, in pixels. + + .. note:: + + This will be ``None`` if :meth:`.is_resolved` is ``False``. + width: Optional[:class:`int`] + The unfurled attachment's width, in pixels. + + .. note:: + + This will be ``None`` if :meth:`.is_resolved` is ``False``. + content_type: Optional[:class:`str`] + The attachment's `media type `_ + + .. note:: + + This will be ``None`` if :meth:`.is_resolved` is ``False``. + loading_state: :class:`MediaLoadingState` + The load state of this attachment on Discord side. + description + """ + + __slots__ = ( + 'url', + 'proxy_url', + 'height', + 'width', + 'content_type', + 'loading_state', + '_resolved', + '_state', + ) + + def __init__(self, ) diff --git a/discord/components.py b/discord/components.py index 2af2d6d20d8b..141c03cc21d8 100644 --- a/discord/components.py +++ b/discord/components.py @@ -4,7 +4,7 @@ Copyright (c) 2015-present Rapptz Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), +copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the @@ -13,7 +13,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER @@ -24,8 +24,24 @@ from __future__ import annotations -from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload -from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, ChannelType, SelectDefaultValueType +from typing import ( + ClassVar, + List, + Literal, + Optional, + TYPE_CHECKING, + Tuple, + Union, +) +from .enums import ( + try_enum, + ComponentType, + ButtonStyle, + TextStyle, + ChannelType, + SelectDefaultValueType, + DividerSize, +) from .utils import get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag @@ -33,14 +49,21 @@ from typing_extensions import Self from .types.components import ( + ComponentBase as ComponentBasePayload, Component as ComponentPayload, ButtonComponent as ButtonComponentPayload, SelectMenu as SelectMenuPayload, SelectOption as SelectOptionPayload, ActionRow as ActionRowPayload, TextInput as TextInputPayload, - ActionRowChildComponent as ActionRowChildComponentPayload, SelectDefaultValues as SelectDefaultValuesPayload, + SectionComponent as SectionComponentPayload, + TextComponent as TextComponentPayload, + ThumbnailComponent as ThumbnailComponentPayload, + MediaGalleryComponent as MediaGalleryComponentPayload, + FileComponent as FileComponentPayload, + DividerComponent as DividerComponentPayload, + ComponentContainer as ComponentContainerPayload, ) from .emoji import Emoji from .abc import Snowflake @@ -56,6 +79,13 @@ 'SelectOption', 'TextInput', 'SelectDefaultValue', + 'SectionComponent', + 'TextComponent', + 'ThumbnailComponent', + 'MediaGalleryComponent', + 'FileComponent', + 'DividerComponent', + 'ComponentContainer', ) @@ -99,7 +129,7 @@ def _raw_construct(cls, **kwargs) -> Self: setattr(self, slot, value) return self - def to_dict(self) -> ComponentPayload: + def to_dict(self) -> ComponentBasePayload: raise NotImplementedError @@ -290,9 +320,13 @@ def __init__(self, data: SelectMenuPayload, /) -> None: self.placeholder: Optional[str] = data.get('placeholder') self.min_values: int = data.get('min_values', 1) self.max_values: int = data.get('max_values', 1) - self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])] + self.options: List[SelectOption] = [ + SelectOption.from_dict(option) for option in data.get('options', []) + ] self.disabled: bool = data.get('disabled', False) - self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])] + self.channel_types: List[ChannelType] = [ + try_enum(ChannelType, t) for t in data.get('channel_types', []) + ] self.default_values: List[SelectDefaultValue] = [ SelectDefaultValue.from_dict(d) for d in data.get('default_values', []) ] @@ -312,7 +346,7 @@ def to_dict(self) -> SelectMenuPayload: if self.channel_types: payload['channel_types'] = [t.value for t in self.channel_types] if self.default_values: - payload["default_values"] = [v.to_dict() for v in self.default_values] + payload['default_values'] = [v.to_dict() for v in self.default_values] return payload @@ -408,7 +442,9 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: elif isinstance(value, _EmojiTag): self._emoji = value._to_partial() else: - raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead') + raise TypeError( + f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead' + ) else: self._emoji = None @@ -564,7 +600,9 @@ def type(self) -> SelectDefaultValueType: @type.setter def type(self, value: SelectDefaultValueType) -> None: if not isinstance(value, SelectDefaultValueType): - raise TypeError(f'expected SelectDefaultValueType, received {value.__class__.__name__} instead') + raise TypeError( + f'expected SelectDefaultValueType, received {value.__class__.__name__} instead' + ) self._type = value @@ -642,17 +680,105 @@ def from_user(cls, user: Snowflake, /) -> Self: ) -@overload -def _component_factory(data: ActionRowChildComponentPayload) -> Optional[ActionRowChildComponentType]: - ... +class SectionComponent(Component): + """Represents a section from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type to create a section is :class:`discord.ui.Section` + not this one. + + .. versionadded:: tbd + + Attributes + ---------- + components: List[Union[:class:`TextDisplay`, :class:`Button`]] + The components on this section. + accessory: Optional[:class:`Component`] + The section accessory. + """ + + def __init__(self, data: SectionComponentPayload) -> None: + self.components: List[Union[TextDisplay, Button]] = [] + + for component_data in data['components']: + component = _component_factory(component_data) + if component is not None: + self.components.append(component) + + try: + self.accessory: Optional[Component] = _component_factory(data['accessory']) + except KeyError: + self.accessory = None + + @property + def type(self) -> Literal[ComponentType.section]: + return ComponentType.section + + def to_dict(self) -> SectionComponentPayload: + payload: SectionComponentPayload = { + 'type': self.type.value, + 'components': [c.to_dict() for c in self.components], + } + if self.accessory: + payload['accessory'] = self.accessory.to_dict() + return payload + +class TextDisplay(Component): + """Represents a text display from the Discord Bot UI Kit. -@overload -def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: - ... + This inherits from :class:`Component`. + + .. versionadded:: tbd + + Parameters + ---------- + content: :class:`str` + The content that this display shows. + """ + + def __init__(self, content: str) -> None: + self.content: str = content + + @property + def type(self) -> Literal[ComponentType.text_display]: + return ComponentType.text_display + + @classmethod + def _from_data(cls, data: TextComponentPayload) -> TextDisplay: + return cls( + content=data['content'], + ) + + def to_dict(self) -> TextComponentPayload: + return { + 'type': self.type.value, + 'content': self.content, + } + + +class ThumbnailComponent(Component): + """Represents a thumbnail display from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructuble and usable type to create a thumbnail + component is :class:`discord.ui.Thumbnail` not this one. + + .. versionadded:: tbd + + Attributes + ---------- + media: :class:`ComponentMedia` + """ -def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory(data: ComponentPayload) -> Optional[Component]: if data['type'] == 1: return ActionRow(data) elif data['type'] == 2: @@ -661,3 +787,17 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti return TextInput(data) elif data['type'] in (3, 5, 6, 7, 8): return SelectMenu(data) + elif data['type'] == 9: + return SectionComponent(data) + elif data['type'] == 10: + return TextDisplay._from_data(data) + elif data['type'] == 11: + return ThumbnailComponent(data) + elif data['type'] == 12: + return MediaGalleryComponent(data) + elif data['type'] == 13: + return FileComponent(data) + elif data['type'] == 14: + return DividerComponent(data) + elif data['type'] == 17: + return ComponentContainer(data) diff --git a/discord/enums.py b/discord/enums.py index ce772cc87285..fc9303d19a69 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -77,6 +77,8 @@ 'VoiceChannelEffectAnimationType', 'SubscriptionStatus', 'MessageReferenceType', + 'DividerSize', + 'MediaLoadingState', ) @@ -641,6 +643,13 @@ class ComponentType(Enum): role_select = 6 mentionable_select = 7 channel_select = 8 + section = 9 + text_display = 10 + thumbnail = 11 + media_gallery = 12 + file = 13 + separator = 14 + container = 17 def __int__(self) -> int: return self.value @@ -863,6 +872,18 @@ class SubscriptionStatus(Enum): inactive = 2 +class DividerSize(Enum): + small = 1 + large = 2 + + +class MediaLoadingState(Enum): + unknown = 0 + loading = 1 + loaded = 2 + not_found = 3 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/flags.py b/discord/flags.py index de806ba9c046..3be3239832dc 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -498,6 +498,14 @@ def forwarded(self): """ return 16384 + @flag_value + def components_v2(self): + """:class:`bool`: Returns ``True`` if the message has Discord's v2 components. + + Does not allow sending any ``content``, ``embed``, or ``embeds``. + """ + return 32768 + @fill_with_flags() class PublicUserFlags(BaseFlags): diff --git a/discord/message.py b/discord/message.py index 3016d2f2945c..1010e1c12a2c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -27,8 +27,6 @@ import asyncio import datetime import re -import io -from os import PathLike from typing import ( Dict, TYPE_CHECKING, @@ -55,7 +53,7 @@ from .components import _component_factory from .embeds import Embed from .member import Member -from .flags import MessageFlags, AttachmentFlags +from .flags import MessageFlags from .file import File from .utils import escape_mentions, MISSING, deprecated from .http import handle_message_parameters @@ -65,6 +63,7 @@ from .threads import Thread from .channel import PartialMessageable from .poll import Poll +from .attachment import Attachment if TYPE_CHECKING: from typing_extensions import Self @@ -108,7 +107,6 @@ __all__ = ( - 'Attachment', 'Message', 'PartialMessage', 'MessageInteraction', @@ -140,298 +138,6 @@ def convert_emoji_reaction(emoji: Union[EmojiInputType, Reaction]) -> str: raise TypeError(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.') -class Attachment(Hashable): - """Represents an attachment from Discord. - - .. container:: operations - - .. describe:: str(x) - - Returns the URL of the attachment. - - .. describe:: x == y - - Checks if the attachment is equal to another attachment. - - .. describe:: x != y - - Checks if the attachment is not equal to another attachment. - - .. describe:: hash(x) - - Returns the hash of the attachment. - - .. versionchanged:: 1.7 - Attachment can now be casted to :class:`str` and is hashable. - - Attributes - ------------ - id: :class:`int` - The attachment ID. - size: :class:`int` - The attachment size in bytes. - height: Optional[:class:`int`] - The attachment's height, in pixels. Only applicable to images and videos. - width: Optional[:class:`int`] - The attachment's width, in pixels. Only applicable to images and videos. - filename: :class:`str` - The attachment's filename. - url: :class:`str` - The attachment URL. If the message this attachment was attached - to is deleted, then this will 404. - proxy_url: :class:`str` - The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the - case of images. When the message is deleted, this URL might be valid for a few - minutes or not valid at all. - content_type: Optional[:class:`str`] - The attachment's `media type `_ - - .. versionadded:: 1.7 - description: Optional[:class:`str`] - The attachment's description. Only applicable to images. - - .. versionadded:: 2.0 - ephemeral: :class:`bool` - Whether the attachment is ephemeral. - - .. versionadded:: 2.0 - duration: Optional[:class:`float`] - The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - waveform: Optional[:class:`bytes`] - The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - title: Optional[:class:`str`] - The normalised version of the attachment's filename. - - .. versionadded:: 2.5 - """ - - __slots__ = ( - 'id', - 'size', - 'height', - 'width', - 'filename', - 'url', - 'proxy_url', - '_http', - 'content_type', - 'description', - 'ephemeral', - 'duration', - 'waveform', - '_flags', - 'title', - ) - - def __init__(self, *, data: AttachmentPayload, state: ConnectionState): - self.id: int = int(data['id']) - self.size: int = data['size'] - self.height: Optional[int] = data.get('height') - self.width: Optional[int] = data.get('width') - self.filename: str = data['filename'] - self.url: str = data['url'] - self.proxy_url: str = data['proxy_url'] - self._http = state.http - self.content_type: Optional[str] = data.get('content_type') - self.description: Optional[str] = data.get('description') - self.ephemeral: bool = data.get('ephemeral', False) - self.duration: Optional[float] = data.get('duration_secs') - self.title: Optional[str] = data.get('title') - - waveform = data.get('waveform') - self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None - - self._flags: int = data.get('flags', 0) - - @property - def flags(self) -> AttachmentFlags: - """:class:`AttachmentFlags`: The attachment's flags.""" - return AttachmentFlags._from_value(self._flags) - - def is_spoiler(self) -> bool: - """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.filename.startswith('SPOILER_') - - def is_voice_message(self) -> bool: - """:class:`bool`: Whether this attachment is a voice message.""" - return self.duration is not None and 'voice-message' in self.url - - def __repr__(self) -> str: - return f'' - - def __str__(self) -> str: - return self.url or '' - - async def save( - self, - fp: Union[io.BufferedIOBase, PathLike[Any]], - *, - seek_begin: bool = True, - use_cached: bool = False, - ) -> int: - """|coro| - - Saves this attachment into a file-like object. - - Parameters - ----------- - fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`] - The file-like object to save this attachment to or the filename - to use. If a filename is passed then a file is created with that - filename and used instead. - seek_begin: :class:`bool` - Whether to seek to the beginning of the file after saving is - successfully done. - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - Raises - -------- - HTTPException - Saving the attachment failed. - NotFound - The attachment was deleted. - - Returns - -------- - :class:`int` - The number of bytes written. - """ - data = await self.read(use_cached=use_cached) - if isinstance(fp, io.BufferedIOBase): - written = fp.write(data) - if seek_begin: - fp.seek(0) - return written - else: - with open(fp, 'wb') as f: - return f.write(data) - - async def read(self, *, use_cached: bool = False) -> bytes: - """|coro| - - Retrieves the content of this attachment as a :class:`bytes` object. - - .. versionadded:: 1.1 - - Parameters - ----------- - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - Raises - ------ - HTTPException - Downloading the attachment failed. - Forbidden - You do not have permissions to access this attachment - NotFound - The attachment was deleted. - - Returns - ------- - :class:`bytes` - The contents of the attachment. - """ - url = self.proxy_url if use_cached else self.url - data = await self._http.get_from_cdn(url) - return data - - async def to_file( - self, - *, - filename: Optional[str] = MISSING, - description: Optional[str] = MISSING, - use_cached: bool = False, - spoiler: bool = False, - ) -> File: - """|coro| - - Converts the attachment into a :class:`File` suitable for sending via - :meth:`abc.Messageable.send`. - - .. versionadded:: 1.3 - - Parameters - ----------- - filename: Optional[:class:`str`] - The filename to use for the file. If not specified then the filename - of the attachment is used instead. - - .. versionadded:: 2.0 - description: Optional[:class:`str`] - The description to use for the file. If not specified then the - description of the attachment is used instead. - - .. versionadded:: 2.0 - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - .. versionadded:: 1.4 - spoiler: :class:`bool` - Whether the file is a spoiler. - - .. versionadded:: 1.4 - - Raises - ------ - HTTPException - Downloading the attachment failed. - Forbidden - You do not have permissions to access this attachment - NotFound - The attachment was deleted. - - Returns - ------- - :class:`File` - The attachment as a file suitable for sending. - """ - - data = await self.read(use_cached=use_cached) - file_filename = filename if filename is not MISSING else self.filename - file_description = description if description is not MISSING else self.description - return File(io.BytesIO(data), filename=file_filename, description=file_description, spoiler=spoiler) - - def to_dict(self) -> AttachmentPayload: - result: AttachmentPayload = { - 'filename': self.filename, - 'id': self.id, - 'proxy_url': self.proxy_url, - 'size': self.size, - 'url': self.url, - 'spoiler': self.is_spoiler(), - } - if self.height: - result['height'] = self.height - if self.width: - result['width'] = self.width - if self.content_type: - result['content_type'] = self.content_type - if self.description is not None: - result['description'] = self.description - return result - - class DeletedReferencedMessage: """A special sentinel type given when the resolved message reference points to a deleted message. diff --git a/discord/types/attachment.py b/discord/types/attachment.py new file mode 100644 index 000000000000..38d8ad667cac --- /dev/null +++ b/discord/types/attachment.py @@ -0,0 +1,58 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import Literal, Optional, TypedDict +from typing_extensions import NotRequired + +from .snowflake import Snowflake + +LoadingState = Literal[0, 1, 2, 3] + +class AttachmentBase(TypedDict): + url: str + proxy_url: str + description: NotRequired[str] + spoiler: NotRequired[bool] + height: NotRequired[Optional[int]] + width: NotRequired[Optional[int]] + content_type: NotRequired[str] + flags: NotRequired[int] + + +class Attachment(AttachmentBase): + id: Snowflake + filename: str + size: int + ephemeral: NotRequired[bool] + duration_secs: NotRequired[float] + waveform: NotRequired[str] + + +class UnfurledAttachment(AttachmentBase): + loading_state: LoadingState + src_is_animated: NotRequired[bool] + placeholder: str + placeholder_version: int diff --git a/discord/types/components.py b/discord/types/components.py index 3b1295c1393c..4521f2514bef 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -29,19 +29,27 @@ from .emoji import PartialEmoji from .channel import ChannelType +from .attachment import UnfurledAttachment ComponentType = Literal[1, 2, 3, 4] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] TextStyle = Literal[1, 2] -DefaultValueType = Literal['user', 'role', 'channel'] +DefaultValueType = Literal["user", "role", "channel"] +DividerSize = Literal[1, 2] +MediaItemLoadingState = Literal[0, 1, 2, 3] -class ActionRow(TypedDict): +class ComponentBase(TypedDict): + id: NotRequired[int] + type: int + + +class ActionRow(ComponentBase): type: Literal[1] components: List[ActionRowChildComponent] -class ButtonComponent(TypedDict): +class ButtonComponent(ComponentBase): type: Literal[2] style: ButtonStyle custom_id: NotRequired[str] @@ -52,7 +60,7 @@ class ButtonComponent(TypedDict): sku_id: NotRequired[str] -class SelectOption(TypedDict): +class SelectOption(ComponentBase): label: str value: str default: bool @@ -60,7 +68,7 @@ class SelectOption(TypedDict): emoji: NotRequired[PartialEmoji] -class SelectComponent(TypedDict): +class SelectComponent(ComponentBase): custom_id: str placeholder: NotRequired[str] min_values: NotRequired[int] @@ -99,7 +107,7 @@ class ChannelSelectComponent(SelectComponent): default_values: NotRequired[List[SelectDefaultValues]] -class TextInput(TypedDict): +class TextInput(ComponentBase): type: Literal[4] custom_id: str style: TextStyle @@ -118,5 +126,52 @@ class SelectMenu(SelectComponent): default_values: NotRequired[List[SelectDefaultValues]] +class SectionComponent(ComponentBase): + type: Literal[9] + components: List[Union[TextComponent, ButtonComponent]] + accessory: NotRequired[ComponentBase] + + +class TextComponent(ComponentBase): + type: Literal[10] + content: str + + +class ThumbnailComponent(ComponentBase, UnfurledAttachment): + type: Literal[11] + + +class MediaGalleryComponent(ComponentBase): + type: Literal[12] + items: List[MediaItem] + + +class FileComponent(ComponentBase): + type: Literal[13] + file: MediaItem + spoiler: NotRequired[bool] + + +class DividerComponent(ComponentBase): + type: Literal[14] + divider: NotRequired[bool] + spacing: NotRequired[DividerSize] + + +class ComponentContainer(ComponentBase): + type: Literal[17] + accent_color: NotRequired[int] + spoiler: NotRequired[bool] + components: List[ContainerComponent] + + ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] -Component = Union[ActionRow, ActionRowChildComponent] +ContainerComponent = Union[ + ActionRow, + TextComponent, + MediaGalleryComponent, + FileComponent, + SectionComponent, + SectionComponent, +] +Component = Union[ActionRowChildComponent, ContainerComponent] diff --git a/discord/types/message.py b/discord/types/message.py index ae38db46f8c0..81bfdd23baed 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -33,11 +33,12 @@ from .emoji import PartialEmoji from .embed import Embed from .channel import ChannelType -from .components import Component +from .components import ComponentBase from .interactions import MessageInteraction, MessageInteractionMetadata from .sticker import StickerItem from .threads import Thread from .poll import Poll +from .attachment import Attachment class PartialMessage(TypedDict): @@ -69,23 +70,6 @@ class Reaction(TypedDict): burst_colors: List[str] -class Attachment(TypedDict): - id: Snowflake - filename: str - size: int - url: str - proxy_url: str - height: NotRequired[Optional[int]] - width: NotRequired[Optional[int]] - description: NotRequired[str] - content_type: NotRequired[str] - spoiler: NotRequired[bool] - ephemeral: NotRequired[bool] - duration_secs: NotRequired[float] - waveform: NotRequired[str] - flags: NotRequired[int] - - MessageActivityType = Literal[1, 2, 3, 5] @@ -189,7 +173,7 @@ class MessageSnapshot(TypedDict): mentions: List[UserWithMember] mention_roles: SnowflakeList sticker_items: NotRequired[List[StickerItem]] - components: NotRequired[List[Component]] + components: NotRequired[List[ComponentBase]] class Message(PartialMessage): @@ -221,7 +205,7 @@ class Message(PartialMessage): referenced_message: NotRequired[Optional[Message]] interaction: NotRequired[MessageInteraction] # deprecated, use interaction_metadata interaction_metadata: NotRequired[MessageInteractionMetadata] - components: NotRequired[List[Component]] + components: NotRequired[List[ComponentBase]] position: NotRequired[int] role_subscription_data: NotRequired[RoleSubscriptionData] thread: NotRequired[Thread] @@ -229,7 +213,7 @@ class Message(PartialMessage): purchase_notification: NotRequired[PurchaseNotificationResponse] -AllowedMentionType = Literal['roles', 'users', 'everyone'] +AllowedMentionType = Literal["roles", "users", "everyone"] class AllowedMentions(TypedDict): diff --git a/discord/ui/section.py b/discord/ui/section.py new file mode 100644 index 000000000000..0f6f76006401 --- /dev/null +++ b/discord/ui/section.py @@ -0,0 +1,50 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import List, Optional + +from .item import Item + + +class Section(Item): + """Represents a UI section. + + .. versionadded:: tbd + + Parameters + ---------- + accessory: Optional[:class:`Item`] + The accessory to show within this section, displayed on the top right of this section. + """ + + __slots__ = ( + 'accessory', + '_children', + ) + + def __init__(self, *, accessory: Optional[Item]) -> None: + self.accessory: Optional[Item] = accessory + self._children: List[Item] = [] + self._underlying = SectionComponent From 470323493e7e3b790cb1f9d74bf45dd02df5f578 Mon Sep 17 00:00:00 2001 From: Willi <83978878+itswilliboy@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:14:06 +0100 Subject: [PATCH 199/354] Pass BotT type argument to DeferTyping --- discord/ext/commands/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 93303973523a..7198c12064ec 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -751,7 +751,7 @@ async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: else: return await self.send(content, **kwargs) - def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping]: + def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping[BotT]]: """Returns an asynchronous context manager that allows you to send a typing indicator to the destination for an indefinite period of time, or 10 seconds if the context manager is called using ``await``. From 0e4f06103ee20d06fb6c0d64f75b1fc475905b95 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 19 Feb 2025 23:15:07 +0100 Subject: [PATCH 200/354] Fix InteractionCallbackResponse.resource having incorrect state Fix InteractionCallbackResponse.resource being created with a ConnectionState instead of _InteractionMessageState --- discord/interactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions.py b/discord/interactions.py index b9d9a4d11ea7..a983d8ab04f6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -727,7 +727,7 @@ def _update(self, data: InteractionCallbackPayload) -> None: activity_instance = resource.get('activity_instance') if message is not None: self.resource = InteractionMessage( - state=self._state, + state=_InteractionMessageState(self._parent, self._state), # pyright: ignore[reportArgumentType] channel=self._parent.channel, # type: ignore # channel should be the correct type here data=message, ) From 19f02c40b371c2caf6bff686d42966a24e79cccd Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Mon, 24 Feb 2025 11:03:24 +0100 Subject: [PATCH 201/354] Document message types that can have a default message reference --- discord/message.py | 18 +++++++++++++++--- docs/api.rst | 16 ++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/discord/message.py b/discord/message.py index 44431b595eea..05b0da18364b 100644 --- a/discord/message.py +++ b/discord/message.py @@ -610,6 +610,11 @@ class MessageReference: .. versionadded:: 2.5 message_id: Optional[:class:`int`] The id of the message referenced. + This can be ``None`` when this message reference was retrieved from + a system message of one of the following types: + + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` channel_id: :class:`int` The channel id of the message referenced. guild_id: Optional[:class:`int`] @@ -2010,9 +2015,16 @@ class Message(PartialMessage, Hashable): The :class:`TextChannel` or :class:`Thread` that the message was sent from. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~discord.MessageReference`] - The message that this message references. This is only applicable to messages of - type :attr:`MessageType.pins_add`, crossposted messages created by a - followed channel integration, or message replies. + The message that this message references. This is only applicable to + message replies (:attr:`MessageType.reply`), crossposted messages created by + a followed channel integration, forwarded messages, and messages of type: + + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` .. versionadded:: 1.5 diff --git a/docs/api.rst b/docs/api.rst index 934335c5ac70..73e0238fcd6a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3851,17 +3851,25 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 - .. attribute:: reply + .. attribute:: default + + A standard reference used by message replies (:attr:`MessageType.reply`), + crossposted messaged created by a followed channel integration, and messages of type: - A message reply. + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` .. attribute:: forward A forwarded message. - .. attribute:: default + .. attribute:: reply - An alias for :attr:`.reply`. + An alias for :attr:`.default`. .. _discord-api-audit-logs: From a8b4eb1e9b9bd83dedce6b23f055ca02a0b59aae Mon Sep 17 00:00:00 2001 From: dolfies Date: Mon, 24 Feb 2025 05:07:21 -0500 Subject: [PATCH 202/354] Create ScheduledEvent on cache miss in SCHEDULED_EVENT_DELETE --- discord/state.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/discord/state.py b/discord/state.py index c4b71b368ad3..0fbeadea2057 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1553,12 +1553,8 @@ def parse_guild_scheduled_event_update(self, data: gw.GuildScheduledEventUpdateE def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None: guild = self._get_guild(int(data['guild_id'])) if guild is not None: - try: - scheduled_event = guild._scheduled_events.pop(int(data['id'])) - except KeyError: - pass - else: - self.dispatch('scheduled_event_delete', scheduled_event) + scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data)) + self.dispatch('scheduled_event_delete', scheduled_event) else: _log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id']) From 93426da37b0878aee54ca24a6d290b8edb4d06bb Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:48:29 +0100 Subject: [PATCH 203/354] Improve on_timeout FAQ Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- docs/faq.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 0cd8b8ad6b8c..16d03362abef 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -439,7 +439,7 @@ How can I disable all items on timeout? This requires three steps. -1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :meth:`Interaction.original_response`. +1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :attr:`InteractionCallbackResponse.resource`. 2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled. 3. Edit the message we retrieved in step 1 with the newly modified view. @@ -467,7 +467,7 @@ Putting it all together, we can do this in a text command: # Step 1 view.message = await ctx.send('Press me!', view=view) -Application commands do not return a message when you respond with :meth:`InteractionResponse.send_message`, therefore in order to reliably do this we should retrieve the message using :meth:`Interaction.original_response`. +Application commands, when you respond with :meth:`InteractionResponse.send_message`, return an instance of :class:`InteractionCallbackResponse` which contains the message you sent. This is the message you should attach to the view. Putting it all together, using the previous view definition: @@ -477,10 +477,13 @@ Putting it all together, using the previous view definition: async def more_timeout_example(interaction): """Another example to showcase disabling buttons on timing out""" view = MyView() - await interaction.response.send_message('Press me!', view=view) + callback = await interaction.response.send_message('Press me!', view=view) # Step 1 - view.message = await interaction.original_response() + resource = callback.resource + # making sure it's an interaction response message + if isinstance(resource, discord.InteractionMessage): + view.message = resource Application Commands From 75134562fd1be352994721b1e91577cd2b81f798 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:13:21 +0100 Subject: [PATCH 204/354] feat: First components v2 commit --- discord/attachment.py | 178 +++++++++++++++++++----------------- discord/components.py | 6 +- discord/types/attachment.py | 4 +- discord/types/components.py | 4 +- discord/ui/section.py | 7 +- 5 files changed, 104 insertions(+), 95 deletions(-) diff --git a/discord/attachment.py b/discord/attachment.py index 2be4eac1aa73..45dab6c746f9 100644 --- a/discord/attachment.py +++ b/discord/attachment.py @@ -29,12 +29,19 @@ from .mixins import Hashable from .file import File -from .state import ConnectionState from .flags import AttachmentFlags +from .enums import MediaLoadingState, try_enum from . import utils if TYPE_CHECKING: - from .types.attachment import Attachment as AttachmentPayload + from .types.attachment import ( + AttachmentBase as AttachmentBasePayload, + Attachment as AttachmentPayload, + UnfurledAttachment as UnfurledAttachmentPayload, + ) + + from .http import HTTPClient + from .state import ConnectionState MISSING = utils.MISSING @@ -45,7 +52,40 @@ class AttachmentBase: - url: str + + __slots__ = ( + 'url', + 'proxy_url', + 'description', + 'filename', + 'spoiler', + 'height', + 'width', + 'content_type', + '_flags', + '_http', + '_state', + ) + + def __init__(self, data: AttachmentBasePayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self._http: HTTPClient = state.http + self.url: str = data['url'] + self.proxy_url: str = data['proxy_url'] + self.description: Optional[str] = data.get('description') + self.spoiler: bool = data.get('spoiler', False) + self.height: Optional[int] = data.get('height') + self.width: Optional[int] = data.get('width') + self.content_type: Optional[str] = data.get('content_type') + self._flags: int = data.get('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: The attachment's flag value.""" + return AttachmentFlags._from_value(self._flags) + + def __str__(self) -> str: + return self.url or '' async def save( self, @@ -200,6 +240,22 @@ async def to_file( spoiler=spoiler, ) + def to_dict(self): + base = { + 'url': self.url, + 'proxy_url': self.proxy_url, + 'spoiler': self.spoiler, + } + + if self.width: + base['width'] = self.width + if self.height: + base['height'] = self.height + if self.description: + base['description'] = self.description + + return base + class Attachment(Hashable, AttachmentBase): """Represents an attachment from Discord. @@ -268,56 +324,34 @@ class Attachment(Hashable, AttachmentBase): The normalised version of the attachment's filename. .. versionadded:: 2.5 + spoiler: :class:`bool` + Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned + data. + + .. versionadded:: 2.6 """ __slots__ = ( 'id', 'size', - 'height', - 'width', - 'filename', - 'url', - 'proxy_url', - '_http', - 'content_type', - 'description', 'ephemeral', 'duration', 'waveform', - '_flags', 'title', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): self.id: int = int(data['id']) - self.size: int = data['size'] - self.height: Optional[int] = data.get('height') - self.width: Optional[int] = data.get('width') self.filename: str = data['filename'] - self.url: str = data['url'] - self.proxy_url: str = data['proxy_url'] - self._http = state.http - self.content_type: Optional[str] = data.get('content_type') - self.description: Optional[str] = data.get('description') + self.size: int = data['size'] self.ephemeral: bool = data.get('ephemeral', False) self.duration: Optional[float] = data.get('duration_secs') self.title: Optional[str] = data.get('title') - - waveform = data.get('waveform') - self.waveform: Optional[bytes] = ( - utils._base64_to_bytes(waveform) if waveform is not None else None - ) - - self._flags: int = data.get('flags', 0) - - @property - def flags(self) -> AttachmentFlags: - """:class:`AttachmentFlags`: The attachment's flags.""" - return AttachmentFlags._from_value(self._flags) + super().__init__(data, state) def is_spoiler(self) -> bool: """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.filename.startswith('SPOILER_') + return self.spoiler or self.filename.startswith('SPOILER_') def is_voice_message(self) -> bool: """:class:`bool`: Whether this attachment is a voice message.""" @@ -326,33 +360,18 @@ def is_voice_message(self) -> bool: def __repr__(self) -> str: return f'' - def __str__(self) -> str: - return self.url or '' - def to_dict(self) -> AttachmentPayload: - result: AttachmentPayload = { - 'filename': self.filename, - 'id': self.id, - 'proxy_url': self.proxy_url, - 'size': self.size, - 'url': self.url, - 'spoiler': self.is_spoiler(), - } - if self.height: - result['height'] = self.height - if self.width: - result['width'] = self.width - if self.content_type: - result['content_type'] = self.content_type - if self.description is not None: - result['description'] = self.description + result: AttachmentPayload = super().to_dict() # pyright: ignore[reportAssignmentType] + result['id'] = self.id + result['filename'] = self.filename + result['size'] = self.size return result class UnfurledAttachment(AttachmentBase): """Represents an unfurled attachment item from a :class:`Component`. - .. versionadded:: tbd + .. versionadded:: 2.6 .. container:: operations @@ -370,48 +389,35 @@ class UnfurledAttachment(AttachmentBase): Attributes ---------- + height: Optional[:class:`int`] + The attachment's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The attachment's width, in pixels. Only applicable to images and videos. url: :class:`str` - The unfurled attachment URL. - proxy_url: Optional[:class:`str`] - The proxy URL. This is cached version of the :attr:`~UnfurledAttachment.url` in the + The attachment URL. If the message this attachment was attached + to is deleted, then this will 404. + proxy_url: :class:`str` + The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the case of images. When the message is deleted, this URL might be valid for a few minutes or not valid at all. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. - height: Optional[:class:`int`] - The unfurled attachment's height, in pixels. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. - width: Optional[:class:`int`] - The unfurled attachment's width, in pixels. - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. content_type: Optional[:class:`str`] The attachment's `media type `_ - - .. note:: - - This will be ``None`` if :meth:`.is_resolved` is ``False``. + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + spoiler: :class:`bool` + Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned + data. loading_state: :class:`MediaLoadingState` - The load state of this attachment on Discord side. - description + The cache state of this unfurled attachment. """ __slots__ = ( - 'url', - 'proxy_url', - 'height', - 'width', - 'content_type', 'loading_state', - '_resolved', - '_state', ) - def __init__(self, ) + def __init__(self, data: UnfurledAttachmentPayload, state: ConnectionState) -> None: + self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data['loading_state']) + super().__init__(data, state) + + def __repr__(self) -> str: + return f'' diff --git a/discord/components.py b/discord/components.py index a0fd1148d448..1a40d3d0bb09 100644 --- a/discord/components.py +++ b/discord/components.py @@ -690,7 +690,7 @@ class SectionComponent(Component): The user constructible and usable type to create a section is :class:`discord.ui.Section` not this one. - .. versionadded:: tbd + .. versionadded:: 2.6 Attributes ---------- @@ -732,7 +732,7 @@ class TextDisplay(Component): This inherits from :class:`Component`. - .. versionadded:: tbd + .. versionadded:: 2.6 Parameters ---------- @@ -770,7 +770,7 @@ class ThumbnailComponent(Component): The user constructuble and usable type to create a thumbnail component is :class:`discord.ui.Thumbnail` not this one. - .. versionadded:: tbd + .. versionadded:: 2.6 Attributes ---------- diff --git a/discord/types/attachment.py b/discord/types/attachment.py index 38d8ad667cac..20fcd8e1b9ae 100644 --- a/discord/types/attachment.py +++ b/discord/types/attachment.py @@ -49,10 +49,8 @@ class Attachment(AttachmentBase): ephemeral: NotRequired[bool] duration_secs: NotRequired[float] waveform: NotRequired[str] + title: NotRequired[str] class UnfurledAttachment(AttachmentBase): loading_state: LoadingState - src_is_animated: NotRequired[bool] - placeholder: str - placeholder_version: int diff --git a/discord/types/components.py b/discord/types/components.py index 4521f2514bef..c169a52861a1 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -143,12 +143,12 @@ class ThumbnailComponent(ComponentBase, UnfurledAttachment): class MediaGalleryComponent(ComponentBase): type: Literal[12] - items: List[MediaItem] + items: List[UnfurledAttachment] class FileComponent(ComponentBase): type: Literal[13] - file: MediaItem + file: UnfurledAttachment spoiler: NotRequired[bool] diff --git a/discord/ui/section.py b/discord/ui/section.py index 0f6f76006401..fc8a9e142217 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -26,6 +26,9 @@ from typing import List, Optional from .item import Item +from ..components import SectionComponent + +__all__ = ('Section',) class Section(Item): @@ -47,4 +50,6 @@ class Section(Item): def __init__(self, *, accessory: Optional[Item]) -> None: self.accessory: Optional[Item] = accessory self._children: List[Item] = [] - self._underlying = SectionComponent + self._underlying = SectionComponent._raw_construct( + accessory=accessory, + ) From 66f3548f3a84a1272c5afa6c6abfff799269adf4 Mon Sep 17 00:00:00 2001 From: dolfies Date: Fri, 28 Feb 2025 18:00:33 -0500 Subject: [PATCH 205/354] Add defaults for message object parsing --- discord/message.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/message.py b/discord/message.py index 05b0da18364b..e7752fb8d893 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2194,15 +2194,15 @@ def __init__( self._state: ConnectionState = state self.webhook_id: Optional[int] = utils._get_as_snowflake(data, 'webhook_id') self.reactions: List[Reaction] = [Reaction(message=self, data=d) for d in data.get('reactions', [])] - self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data['attachments']] - self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] + self.attachments: List[Attachment] = [Attachment(data=a, state=self._state) for a in data.get('attachments', [])] + self.embeds: List[Embed] = [Embed.from_dict(a) for a in data.get('embeds', [])] self.activity: Optional[MessageActivityPayload] = data.get('activity') - self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) + self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('edited_timestamp')) self.type: MessageType = try_enum(MessageType, data['type']) - self.pinned: bool = data['pinned'] + self.pinned: bool = data.get('pinned', False) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) - self.mention_everyone: bool = data['mention_everyone'] - self.tts: bool = data['tts'] + self.mention_everyone: bool = data.get('mention_everyone', False) + self.tts: bool = data.get('tts', False) self.content: str = data['content'] self.nonce: Optional[Union[int, str]] = data.get('nonce') self.position: Optional[int] = data.get('position') From fbe2b358fc3129902f79c4747d8e21eabf5e0518 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 28 Feb 2025 18:03:47 -0500 Subject: [PATCH 206/354] Add note about NotFound for Messageable.send Fix #10116 --- discord/abc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/abc.py b/discord/abc.py index 70531fb2005e..692472f8fa8c 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1574,6 +1574,9 @@ async def send( Sending the message failed. ~discord.Forbidden You do not have the proper permissions to send the message. + ~discord.NotFound + You sent a message with the same nonce as one that has been explicitly + deleted shortly earlier. ValueError The ``files`` or ``embeds`` list is not of the appropriate size. TypeError From 335b3976d86660e1e1ae345cd161dc8556e9236a Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 10:01:43 +0100 Subject: [PATCH 207/354] chore: Update components --- discord/attachment.py | 25 +++- discord/components.py | 286 +++++++++++++++++++++++++++++++----- discord/enums.py | 4 +- discord/message.py | 2 +- discord/types/attachment.py | 6 +- discord/types/components.py | 29 ++-- discord/ui/__init__.py | 1 + discord/ui/container.py | 86 +++++++++++ discord/ui/section.py | 55 ------- discord/ui/view.py | 11 +- 10 files changed, 394 insertions(+), 111 deletions(-) create mode 100644 discord/ui/container.py delete mode 100644 discord/ui/section.py diff --git a/discord/attachment.py b/discord/attachment.py index 45dab6c746f9..195ce30b5d3c 100644 --- a/discord/attachment.py +++ b/discord/attachment.py @@ -27,6 +27,7 @@ from os import PathLike from typing import TYPE_CHECKING, Any, Optional, Union +from .errors import ClientException from .mixins import Hashable from .file import File from .flags import AttachmentFlags @@ -67,9 +68,9 @@ class AttachmentBase: '_state', ) - def __init__(self, data: AttachmentBasePayload, state: ConnectionState) -> None: - self._state: ConnectionState = state - self._http: HTTPClient = state.http + def __init__(self, data: AttachmentBasePayload, state: Optional[ConnectionState]) -> None: + self._state: Optional[ConnectionState] = state + self._http: Optional[HTTPClient] = state.http if state else None self.url: str = data['url'] self.proxy_url: str = data['proxy_url'] self.description: Optional[str] = data.get('description') @@ -162,12 +163,19 @@ async def read(self, *, use_cached: bool = False) -> bytes: You do not have permissions to access this attachment NotFound The attachment was deleted. + ClientException + Cannot read a stateless attachment. Returns ------- :class:`bytes` The contents of the attachment. """ + if not self._http: + raise ClientException( + 'Cannot read a stateless attachment' + ) + url = self.proxy_url if use_cached else self.url data = await self._http.get_from_cdn(url) return data @@ -240,8 +248,8 @@ async def to_file( spoiler=spoiler, ) - def to_dict(self): - base = { + def to_dict(self) -> AttachmentBasePayload: + base: AttachmentBasePayload = { 'url': self.url, 'proxy_url': self.proxy_url, 'spoiler': self.spoiler, @@ -415,9 +423,12 @@ class UnfurledAttachment(AttachmentBase): 'loading_state', ) - def __init__(self, data: UnfurledAttachmentPayload, state: ConnectionState) -> None: - self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data['loading_state']) + def __init__(self, data: UnfurledAttachmentPayload, state: Optional[ConnectionState]) -> None: + self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data.get('loading_state', 0)) super().__init__(data, state) def __repr__(self) -> str: return f'' + + def to_object_dict(self): + return {'url': self.url} diff --git a/discord/components.py b/discord/components.py index 1a40d3d0bb09..09f6d54abcb0 100644 --- a/discord/components.py +++ b/discord/components.py @@ -33,6 +33,8 @@ Tuple, Union, ) + +from .attachment import UnfurledAttachment from .enums import ( try_enum, ComponentType, @@ -40,8 +42,9 @@ TextStyle, ChannelType, SelectDefaultValueType, - DividerSize, + SeparatorSize, ) +from .colour import Colour from .utils import get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag @@ -59,16 +62,20 @@ SelectDefaultValues as SelectDefaultValuesPayload, SectionComponent as SectionComponentPayload, TextComponent as TextComponentPayload, - ThumbnailComponent as ThumbnailComponentPayload, MediaGalleryComponent as MediaGalleryComponentPayload, FileComponent as FileComponentPayload, - DividerComponent as DividerComponentPayload, - ComponentContainer as ComponentContainerPayload, + SeparatorComponent as SeparatorComponentPayload, + MediaGalleryItem as MediaGalleryItemPayload, + ThumbnailComponent as ThumbnailComponentPayload, + ContainerComponent as ContainerComponentPayload, ) + from .emoji import Emoji from .abc import Snowflake + from .state import ConnectionState ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput'] + SectionComponentType = Union['TextDisplay', 'Button'] __all__ = ( @@ -80,12 +87,10 @@ 'TextInput', 'SelectDefaultValue', 'SectionComponent', - 'TextComponent', 'ThumbnailComponent', 'MediaGalleryComponent', 'FileComponent', - 'DividerComponent', - 'ComponentContainer', + 'SectionComponent', ) @@ -159,7 +164,7 @@ def __init__(self, data: ActionRowPayload, /) -> None: component = _component_factory(component_data) if component is not None: - self.children.append(component) + self.children.append(component) # type: ignore # should be the correct type here @property def type(self) -> Literal[ComponentType.action_row]: @@ -701,12 +706,12 @@ class SectionComponent(Component): """ def __init__(self, data: SectionComponentPayload) -> None: - self.components: List[Union[TextDisplay, Button]] = [] + self.components: List[SectionComponentType] = [] for component_data in data['components']: component = _component_factory(component_data) if component is not None: - self.components.append(component) + self.components.append(component) # type: ignore # should be the correct type here try: self.accessory: Optional[Component] = _component_factory(data['accessory']) @@ -727,6 +732,43 @@ def to_dict(self) -> SectionComponentPayload: return payload +class ThumbnailComponent(Component): + """Represents a Thumbnail from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + Attributes + ---------- + media: :class:`UnfurledAttachment` + The media for this thumbnail. + description: Optional[:class:`str`] + The description shown within this thumbnail. + spoiler: :class:`bool` + Whether this thumbnail is flagged as a spoiler. + """ + + def __init__( + self, + data: ThumbnailComponentPayload, + state: ConnectionState, + ) -> None: + self.media: UnfurledAttachment = UnfurledAttachment(data['media'], state) + self.description: Optional[str] = data.get('description') + self.spoiler: bool = data.get('spoiler', False) + + @property + def type(self) -> Literal[ComponentType.thumbnail]: + return ComponentType.thumbnail + + def to_dict(self) -> ThumbnailComponentPayload: + return { + 'media': self.media.to_dict(), # type: ignroe + 'description': self.description, + 'spoiler': self.spoiler, + 'type': self.type.value, + } + + class TextDisplay(Component): """Represents a text display from the Discord Bot UI Kit. @@ -734,51 +776,231 @@ class TextDisplay(Component): .. versionadded:: 2.6 - Parameters + Attributes ---------- content: :class:`str` The content that this display shows. """ - def __init__(self, content: str) -> None: - self.content: str = content + def __init__(self, data: TextComponentPayload) -> None: + self.content: str = data['content'] @property def type(self) -> Literal[ComponentType.text_display]: return ComponentType.text_display + def to_dict(self) -> TextComponentPayload: + return { + 'type': self.type.value, + 'content': self.content, + } + + +class MediaGalleryItem: + """Represents a :class:`MediaGalleryComponent` media item. + + Parameters + ---------- + url: :class:`str` + The url of the media item. This can be a local file uploaded + as an attachment in the message, that can be accessed using + the ``attachment://file-name.extension`` format. + description: Optional[:class:`str`] + The description to show within this item. + spoiler: :class:`bool` + Whether this item should be flagged as a spoiler. + """ + + __slots__ = ( + 'url', + 'description', + 'spoiler', + '_state', + ) + + def __init__( + self, + url: str, + *, + description: Optional[str] = None, + spoiler: bool = False, + ) -> None: + self.url: str = url + self.description: Optional[str] = description + self.spoiler: bool = spoiler + self._state: Optional[ConnectionState] = None + @classmethod - def _from_data(cls, data: TextComponentPayload) -> TextDisplay: - return cls( - content=data['content'], + def _from_data( + cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState] + ) -> MediaGalleryItem: + media = data['media'] + self = cls( + url=media['url'], + description=data.get('description'), + spoiler=data.get('spoiler', False), ) + self._state = state + return self - def to_dict(self) -> TextComponentPayload: + @classmethod + def _from_gallery( + cls, + items: List[MediaGalleryItemPayload], + state: Optional[ConnectionState], + ) -> List[MediaGalleryItem]: + return [cls._from_data(item, state) for item in items] + + def to_dict(self) -> MediaGalleryItemPayload: + return { # type: ignore + 'media': {'url': self.url}, + 'description': self.description, + 'spoiler': self.spoiler, + } + + +class MediaGalleryComponent(Component): + """Represents a Media Gallery component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The items this gallery has. + """ + + __slots__ = ('items', 'id') + + def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None: + self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state) + + @property + def type(self) -> Literal[ComponentType.media_gallery]: + return ComponentType.media_gallery + + def to_dict(self) -> MediaGalleryComponentPayload: return { + 'id': self.id, 'type': self.type.value, - 'content': self.content, + 'items': [item.to_dict() for item in self.items], } -class ThumbnailComponent(Component): - """Represents a thumbnail display from the Discord Bot UI Kit. +class FileComponent(Component): + """Represents a File component from the Discord Bot UI Kit. This inherits from :class:`Component`. - .. note:: + Attributes + ---------- + media: :class:`UnfurledAttachment` + The unfurled attachment contents of the file. + spoiler: :class:`bool` + Whether this file is flagged as a spoiler. + """ + + __slots__ = ( + 'media', + 'spoiler', + ) - The user constructuble and usable type to create a thumbnail - component is :class:`discord.ui.Thumbnail` not this one. + def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: + self.media: UnfurledAttachment = UnfurledAttachment( + data['file'], state, + ) + self.spoiler: bool = data.get('spoiler', False) - .. versionadded:: 2.6 + @property + def type(self) -> Literal[ComponentType.file]: + return ComponentType.file + + def to_dict(self) -> FileComponentPayload: + return { # type: ignore + 'file': {'url': self.url}, + 'spoiler': self.spoiler, + 'type': self.type.value, + } + + +class SeparatorComponent(Component): + """Represents a Separator from the Discord Bot UI Kit. + + This inherits from :class:`Component`. Attributes ---------- - media: :class:`ComponentMedia` + spacing: :class:`SeparatorSize` + The spacing size of the separator. + divider: :class:`bool` + Whether this separator is a divider. """ + __slots__ = ( + 'spacing', + 'divider', + ) + + def __init__( + self, + data: SeparatorComponentPayload, + ) -> None: + self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1)) + self.divider: bool = data.get('divider', True) + + @property + def type(self) -> Literal[ComponentType.separator]: + return ComponentType.separator + + def to_dict(self) -> SeparatorComponentPayload: + return { + 'type': self.type.value, + 'divider': self.divider, + 'spacing': self.spacing.value, + } + + +class Container(Component): + """Represents a Container from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + Attributes + ---------- + children: :class:`Component` + This container's children. + spoiler: :class:`bool` + Whether this container is flagged as a spoiler. + """ + + def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None: + self.children: List[Component] = [] + + for child in data['components']: + comp = _component_factory(child, state) + + if comp: + self.children.append(comp) + + self.spoiler: bool = data.get('spoiler', False) + self._colour: Optional[Colour] + try: + self._colour = Colour(data['accent_color']) + except KeyError: + self._colour = None + + @property + def accent_colour(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The container's accent colour.""" + return self._colour + + accent_color = accent_colour + """Optional[:class:`Color`]: The container's accent color.""" + -def _component_factory(data: ComponentPayload) -> Optional[Component]: +def _component_factory( + data: ComponentPayload, state: Optional[ConnectionState] = None +) -> Optional[Component]: if data['type'] == 1: return ActionRow(data) elif data['type'] == 2: @@ -790,14 +1012,12 @@ def _component_factory(data: ComponentPayload) -> Optional[Component]: elif data['type'] == 9: return SectionComponent(data) elif data['type'] == 10: - return TextDisplay._from_data(data) - elif data['type'] == 11: - return ThumbnailComponent(data) + return TextDisplay(data) elif data['type'] == 12: - return MediaGalleryComponent(data) + return MediaGalleryComponent(data, state) elif data['type'] == 13: - return FileComponent(data) + return FileComponent(data, state) elif data['type'] == 14: - return DividerComponent(data) + return SeparatorComponent(data) elif data['type'] == 17: - return ComponentContainer(data) + return Container(data, state) diff --git a/discord/enums.py b/discord/enums.py index 082a1a708247..025f0bf147c1 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -77,7 +77,7 @@ 'VoiceChannelEffectAnimationType', 'SubscriptionStatus', 'MessageReferenceType', - 'DividerSize', + 'SeparatorSize', 'MediaLoadingState', ) @@ -872,7 +872,7 @@ class SubscriptionStatus(Enum): inactive = 2 -class DividerSize(Enum): +class SeparatorSize(Enum): small = 1 large = 2 diff --git a/discord/message.py b/discord/message.py index 8a916083e978..000747e787cf 100644 --- a/discord/message.py +++ b/discord/message.py @@ -238,7 +238,7 @@ def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): self.components: List[MessageComponentType] = [] for component_data in data.get('components', []): - component = _component_factory(component_data) + component = _component_factory(component_data, state) if component is not None: self.components.append(component) diff --git a/discord/types/attachment.py b/discord/types/attachment.py index 20fcd8e1b9ae..0084c334c67e 100644 --- a/discord/types/attachment.py +++ b/discord/types/attachment.py @@ -25,7 +25,7 @@ from __future__ import annotations from typing import Literal, Optional, TypedDict -from typing_extensions import NotRequired +from typing_extensions import NotRequired, Required from .snowflake import Snowflake @@ -52,5 +52,5 @@ class Attachment(AttachmentBase): title: NotRequired[str] -class UnfurledAttachment(AttachmentBase): - loading_state: LoadingState +class UnfurledAttachment(AttachmentBase, total=False): + loading_state: Required[LoadingState] diff --git a/discord/types/components.py b/discord/types/components.py index c169a52861a1..cffb67ead3be 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -24,14 +24,14 @@ from __future__ import annotations -from typing import List, Literal, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired from .emoji import PartialEmoji from .channel import ChannelType from .attachment import UnfurledAttachment -ComponentType = Literal[1, 2, 3, 4] +ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] TextStyle = Literal[1, 2] DefaultValueType = Literal["user", "role", "channel"] @@ -137,13 +137,22 @@ class TextComponent(ComponentBase): content: str -class ThumbnailComponent(ComponentBase, UnfurledAttachment): +class ThumbnailComponent(ComponentBase): type: Literal[11] + media: UnfurledAttachment + description: NotRequired[Optional[str]] + spoiler: NotRequired[bool] + + +class MediaGalleryItem(TypedDict): + media: UnfurledAttachment + description: NotRequired[Optional[str]] + spoiler: NotRequired[bool] class MediaGalleryComponent(ComponentBase): type: Literal[12] - items: List[UnfurledAttachment] + items: List[MediaGalleryItem] class FileComponent(ComponentBase): @@ -152,26 +161,28 @@ class FileComponent(ComponentBase): spoiler: NotRequired[bool] -class DividerComponent(ComponentBase): +class SeparatorComponent(ComponentBase): type: Literal[14] divider: NotRequired[bool] spacing: NotRequired[DividerSize] -class ComponentContainer(ComponentBase): +class ContainerComponent(ComponentBase): type: Literal[17] accent_color: NotRequired[int] spoiler: NotRequired[bool] - components: List[ContainerComponent] + components: List[ContainerChildComponent] ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] -ContainerComponent = Union[ +ContainerChildComponent = Union[ ActionRow, TextComponent, MediaGalleryComponent, FileComponent, SectionComponent, SectionComponent, + ContainerComponent, + SeparatorComponent, ] -Component = Union[ActionRowChildComponent, ContainerComponent] +Component = Union[ActionRowChildComponent, ContainerChildComponent] diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index c5a51777ce3e..029717cb5294 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -16,3 +16,4 @@ from .select import * from .text_input import * from .dynamic import * +from .container import * diff --git a/discord/ui/container.py b/discord/ui/container.py new file mode 100644 index 000000000000..6792c188f089 --- /dev/null +++ b/discord/ui/container.py @@ -0,0 +1,86 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from ..components import Component + from ..colour import Colour, Color + +__all__ = ('Container',) + + +class Container: + """Represents a Components V2 Container. + + .. versionadded:: 2.6 + + Parameters + ---------- + children: List[:class:`Item`] + The initial children of this container. + accent_colour: Optional[:class:`~discord.Colour`] + The colour of the container. Defaults to ``None``. + accent_color: Optional[:class:`~discord.Color`] + The color of the container. Defaults to ``None``. + spoiler: :class:`bool` + Whether to flag this container as a spoiler. Defaults + to ``False``. + """ + + __discord_ui_container__ = True + + def __init__( + self, + children: List[Component], + *, + accent_colour: Optional[Colour] = None, + accent_color: Optional[Color] = None, + spoiler: bool = False, + ) -> None: + self._children: List[Component] = children + self.spoiler: bool = spoiler + self._colour = accent_colour or accent_color + + @property + def children(self) -> List[Component]: + """List[:class:`~discord.Component`]: The children of this container.""" + return self._children.copy() + + @children.setter + def children(self, value: List[Component]) -> None: + self._children = value + + @property + def accent_colour(self) -> Optional[Colour]: + """Optional[:class:`~discord.Colour`]: The colour of the container, or ``None``.""" + return self._colour + + @accent_colour.setter + def accent_colour(self, value: Optional[Colour]) -> None: + self._colour = value + + accent_color = accent_colour + """Optional[:class:`~discord.Color`]: The color of the container, or ``None``.""" diff --git a/discord/ui/section.py b/discord/ui/section.py deleted file mode 100644 index fc8a9e142217..000000000000 --- a/discord/ui/section.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-present Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" -from __future__ import annotations - -from typing import List, Optional - -from .item import Item -from ..components import SectionComponent - -__all__ = ('Section',) - - -class Section(Item): - """Represents a UI section. - - .. versionadded:: tbd - - Parameters - ---------- - accessory: Optional[:class:`Item`] - The accessory to show within this section, displayed on the top right of this section. - """ - - __slots__ = ( - 'accessory', - '_children', - ) - - def __init__(self, *, accessory: Optional[Item]) -> None: - self.accessory: Optional[Item] = accessory - self._children: List[Item] = [] - self._underlying = SectionComponent._raw_construct( - accessory=accessory, - ) diff --git a/discord/ui/view.py b/discord/ui/view.py index dd44944ec0ef..b6262cf22b1c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -40,6 +40,11 @@ _component_factory, Button as ButtonComponent, SelectMenu as SelectComponent, + SectionComponent, + TextDisplay, + MediaGalleryComponent, + FileComponent, + SeparatorComponent, ) # fmt: off @@ -62,6 +67,7 @@ _log = logging.getLogger(__name__) +V2_COMPONENTS = (SectionComponent, TextDisplay, MediaGalleryComponent, FileComponent, SeparatorComponent) def _walk_all_components(components: List[Component]) -> Iterator[Component]: @@ -81,6 +87,8 @@ def _component_to_item(component: Component) -> Item: from .select import BaseSelect return BaseSelect.from_component(component) + if isinstance(component, V2_COMPONENTS): + return component return Item.from_component(component) @@ -157,6 +165,7 @@ class View: __discord_ui_view__: ClassVar[bool] = True __discord_ui_modal__: ClassVar[bool] = False + __discord_ui_container__: ClassVar[bool] = False __view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] def __init_subclass__(cls) -> None: @@ -737,7 +746,7 @@ def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> components: List[Component] = [] for component_data in data: - component = _component_factory(component_data) + component = _component_factory(component_data, self._state) if component is not None: components.append(component) From ce3f48e959662ce409d46042d65514b958461204 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 10:03:04 +0100 Subject: [PATCH 208/354] fix: License quotes --- discord/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index 09f6d54abcb0..4b25bcb00156 100644 --- a/discord/components.py +++ b/discord/components.py @@ -4,7 +4,7 @@ Copyright (c) 2015-present Rapptz Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the 'Software'), +copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the @@ -13,7 +13,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER From eea95d95c917cc3cda267386d2cad623bfef4b20 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 12:55:15 +0100 Subject: [PATCH 209/354] chore: Add more components and some things on weights and so --- discord/components.py | 43 +++++++--- discord/http.py | 6 ++ discord/types/components.py | 1 + discord/ui/container.py | 55 ++++++++++--- discord/ui/item.py | 3 + discord/ui/section.py | 151 ++++++++++++++++++++++++++++++++++++ discord/ui/thumbnail.py | 86 ++++++++++++++++++++ discord/ui/view.py | 55 +++++++------ 8 files changed, 358 insertions(+), 42 deletions(-) create mode 100644 discord/ui/section.py create mode 100644 discord/ui/thumbnail.py diff --git a/discord/components.py b/discord/components.py index 4b25bcb00156..4e0196f7dd92 100644 --- a/discord/components.py +++ b/discord/components.py @@ -97,12 +97,19 @@ class Component: """Represents a Discord Bot UI Kit Component. - Currently, the only components supported by Discord are: + The components supported by Discord are: - :class:`ActionRow` - :class:`Button` - :class:`SelectMenu` - :class:`TextInput` + - :class:`SectionComponent` + - :class:`TextDisplay` + - :class:`ThumbnailComponent` + - :class:`MediaGalleryComponent` + - :class:`FileComponent` + - :class:`SeparatorComponent` + - :class:`Container` This class is abstract and cannot be instantiated. @@ -705,11 +712,18 @@ class SectionComponent(Component): The section accessory. """ - def __init__(self, data: SectionComponentPayload) -> None: + __slots__ = ( + 'components', + 'accessory', + ) + + __repr_info__ = __slots__ + + def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: self.components: List[SectionComponentType] = [] for component_data in data['components']: - component = _component_factory(component_data) + component = _component_factory(component_data, state) if component is not None: self.components.append(component) # type: ignore # should be the correct type here @@ -737,6 +751,11 @@ class ThumbnailComponent(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type to create a thumbnail is :class:`discord.ui.Thumbnail` + not this one. + Attributes ---------- media: :class:`UnfurledAttachment` @@ -747,10 +766,12 @@ class ThumbnailComponent(Component): Whether this thumbnail is flagged as a spoiler. """ + __slots__ = () + def __init__( self, data: ThumbnailComponentPayload, - state: ConnectionState, + state: Optional[ConnectionState], ) -> None: self.media: UnfurledAttachment = UnfurledAttachment(data['media'], state) self.description: Optional[str] = data.get('description') @@ -932,13 +953,13 @@ class SeparatorComponent(Component): ---------- spacing: :class:`SeparatorSize` The spacing size of the separator. - divider: :class:`bool` - Whether this separator is a divider. + visible: :class:`bool` + Whether this separator is visible and shows a divider. """ __slots__ = ( 'spacing', - 'divider', + 'visible', ) def __init__( @@ -946,7 +967,7 @@ def __init__( data: SeparatorComponentPayload, ) -> None: self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1)) - self.divider: bool = data.get('divider', True) + self.visible: bool = data.get('divider', True) @property def type(self) -> Literal[ComponentType.separator]: @@ -955,7 +976,7 @@ def type(self) -> Literal[ComponentType.separator]: def to_dict(self) -> SeparatorComponentPayload: return { 'type': self.type.value, - 'divider': self.divider, + 'divider': self.visible, 'spacing': self.spacing.value, } @@ -1010,9 +1031,11 @@ def _component_factory( elif data['type'] in (3, 5, 6, 7, 8): return SelectMenu(data) elif data['type'] == 9: - return SectionComponent(data) + return SectionComponent(data, state) elif data['type'] == 10: return TextDisplay(data) + elif data['type'] == 11: + return ThumbnailComponent(data, state) elif data['type'] == 12: return MediaGalleryComponent(data, state) elif data['type'] == 13: diff --git a/discord/http.py b/discord/http.py index 6617efa2708b..58b50172234e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -193,6 +193,12 @@ def handle_message_parameters( if view is not MISSING: if view is not None: payload['components'] = view.to_components() + + if view.has_components_v2(): + if flags is not MISSING: + flags.components_v2 = True + else: + flags = MessageFlags(components_v2=True) else: payload['components'] = [] diff --git a/discord/types/components.py b/discord/types/components.py index cffb67ead3be..a50cbdd1ec44 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -184,5 +184,6 @@ class ContainerComponent(ComponentBase): SectionComponent, ContainerComponent, SeparatorComponent, + ThumbnailComponent, ] Component = Union[ActionRowChildComponent, ContainerChildComponent] diff --git a/discord/ui/container.py b/discord/ui/container.py index 6792c188f089..4bd68b724496 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,23 +23,32 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar + +from .item import Item +from ..enums import ComponentType if TYPE_CHECKING: - from ..components import Component + from typing_extensions import Self + + from .view import View + from ..colour import Colour, Color + from ..components import Container as ContainerComponent + +V = TypeVar('V', bound='View', covariant=True) __all__ = ('Container',) -class Container: +class Container(Item[V]): """Represents a Components V2 Container. .. versionadded:: 2.6 Parameters ---------- - children: List[:class:`Item`] + children: List[:class:`Item`] The initial children of this container. accent_colour: Optional[:class:`~discord.Colour`] The colour of the container. Defaults to ``None``. @@ -48,29 +57,31 @@ class Container: spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults to ``False``. + timeout: Optional[:class:`float`] + The timeout to set to this container items. Defaults to ``180``. """ __discord_ui_container__ = True def __init__( self, - children: List[Component], + children: List[Item[Any]], *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, ) -> None: - self._children: List[Component] = children + self._children: List[Item[Any]] = children self.spoiler: bool = spoiler self._colour = accent_colour or accent_color @property - def children(self) -> List[Component]: - """List[:class:`~discord.Component`]: The children of this container.""" + def children(self) -> List[Item[Any]]: + """List[:class:`Item`]: The children of this container.""" return self._children.copy() @children.setter - def children(self, value: List[Component]) -> None: + def children(self, value: List[Item[Any]]) -> None: self._children = value @property @@ -84,3 +95,29 @@ def accent_colour(self, value: Optional[Colour]) -> None: accent_color = accent_colour """Optional[:class:`~discord.Color`]: The color of the container, or ``None``.""" + + @property + def type(self) -> Literal[ComponentType.container]: + return ComponentType.container + + def _is_v2(self) -> bool: + return True + + def to_component_dict(self) -> Dict[str, Any]: + base = { + 'type': self.type.value, + 'spoiler': self.spoiler, + 'components': [c.to_component_dict() for c in self._children] + } + if self._colour is not None: + base['accent_color'] = self._colour.value + return base + + @classmethod + def from_component(cls, component: ContainerComponent) -> Self: + from .view import _component_to_item + return cls( + children=[_component_to_item(c) for c in component.children], + accent_colour=component.accent_colour, + spoiler=component.spoiler, + ) diff --git a/discord/ui/item.py b/discord/ui/item.py index 1ee5492836b5..2d2a3aaa6f88 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -80,6 +80,9 @@ def _refresh_component(self, component: Component) -> None: def _refresh_state(self, interaction: Interaction, data: Dict[str, Any]) -> None: return None + def _is_v2(self) -> bool: + return False + @classmethod def from_component(cls: Type[I], component: Component) -> I: return cls() diff --git a/discord/ui/section.py b/discord/ui/section.py new file mode 100644 index 000000000000..81a0e4ba4fc0 --- /dev/null +++ b/discord/ui/section.py @@ -0,0 +1,151 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union + +from .item import Item +from .text_display import TextDisplay +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + from ..components import SectionComponent + +V = TypeVar('V', bound='View', covariant=True) + + +class Section(Item[V]): + """Represents a UI section. + + .. versionadded:: 2.6 + + Parameters + ---------- + children: List[Union[:class:`str`, :class:`TextDisplay`]] + The text displays of this section. Up to 3. + accessory: Optional[:class:`Item`] + The section accessory. Defaults to ``None``. + """ + + __slots__ = ( + '_children', + 'accessory', + ) + + def __init__( + self, + children: List[Union[TextDisplay[Any], str]], + *, + accessory: Optional[Item[Any]] = None, + ) -> None: + if len(children) > 3: + raise ValueError('maximum number of children exceeded') + self._children: List[TextDisplay[Any]] = [ + c if isinstance(c, TextDisplay) else TextDisplay(c) for c in children + ] + self.accessory: Optional[Item[Any]] = accessory + + @property + def type(self) -> Literal[ComponentType.section]: + return ComponentType.section + + def _is_v2(self) -> bool: + return True + + def add_item(self, item: Union[str, TextDisplay[Any]]) -> Self: + """Adds an item to this section. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: Union[:class:`str`, :class:`TextDisplay`] + The text display to add. + + Raises + ------ + TypeError + A :class:`TextDisplay` was not passed. + ValueError + Maximum number of children has been exceeded (3). + """ + + if len(self._children) >= 3: + raise ValueError('maximum number of children exceeded') + + if not isinstance(item, (TextDisplay, str)): + raise TypeError(f'expected TextDisplay or str not {item.__class__.__name__}') + + self._children.append( + item if isinstance(item, TextDisplay) else TextDisplay(item), + ) + return self + + def remove_item(self, item: TextDisplay[Any]) -> Self: + """Removes an item from this section. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`TextDisplay` + The item to remove from the section. + """ + + try: + self._children.remove(item) + except ValueError: + pass + return self + + def clear_items(self) -> Self: + """Removes all the items from the section. + + This function returns the class instance to allow for fluent-style + chaining. + """ + self._children.clear() + return self + + @classmethod + def from_component(cls, component: SectionComponent) -> Self: + from .view import _component_to_item # >circular import< + return cls( + children=[_component_to_item(c) for c in component.components], + accessory=_component_to_item(component.accessory) if component.accessory else None, + ) + + def to_component_dict(self) -> Dict[str, Any]: + data = { + 'components': [c.to_component_dict() for c in self._children], + 'type': self.type.value, + } + if self.accessory: + data['accessory'] = self.accessory.to_component_dict() + return data diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py new file mode 100644 index 000000000000..a984a1892f9c --- /dev/null +++ b/discord/ui/thumbnail.py @@ -0,0 +1,86 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar + +from .item import Item +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + from ..components import ThumbnailComponent + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ( + 'Thumbnail', +) + +class Thumbnail(Item[V]): + """Represents a UI Thumbnail. + + .. versionadded:: 2.6 + + Parameters + ---------- + url: :class:`str` + The URL of the thumbnail. This can only point to a local attachment uploaded + within this item. URLs must match the ``attachment://file-name.extension`` + structure. + description: Optional[:class:`str`] + The description of this thumbnail. Defaults to ``None``. + spoiler: :class:`bool` + Whether to flag this thumbnail as a spoiler. Defaults to ``False``. + """ + + def __init__(self, url: str, *, description: Optional[str] = None, spoiler: bool = False) -> None: + self.url: str = url + self.description: Optional[str] = description + self.spoiler: bool = spoiler + + @property + def type(self) -> Literal[ComponentType.thumbnail]: + return ComponentType.thumbnail + + def _is_v2(self) -> bool: + return True + + def to_component_dict(self) -> Dict[str, Any]: + return { + 'type': self.type.value, + 'spoiler': self.spoiler, + 'media': {'url': self.url}, + 'description': self.description, + } + + @classmethod + def from_component(cls, component: ThumbnailComponent) -> Self: + return cls( + url=component.media.url, + description=component.description, + spoiler=component.spoiler, + ) diff --git a/discord/ui/view.py b/discord/ui/view.py index b6262cf22b1c..4abac51161f6 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -41,7 +41,7 @@ Button as ButtonComponent, SelectMenu as SelectComponent, SectionComponent, - TextDisplay, + TextDisplay as TextDisplayComponent, MediaGalleryComponent, FileComponent, SeparatorComponent, @@ -67,7 +67,6 @@ _log = logging.getLogger(__name__) -V2_COMPONENTS = (SectionComponent, TextDisplay, MediaGalleryComponent, FileComponent, SeparatorComponent) def _walk_all_components(components: List[Component]) -> Iterator[Component]: @@ -87,8 +86,7 @@ def _component_to_item(component: Component) -> Item: from .select import BaseSelect return BaseSelect.from_component(component) - if isinstance(component, V2_COMPONENTS): - return component + # TODO: convert V2 Components into Item's return Item.from_component(component) @@ -97,11 +95,13 @@ class _ViewWeights: # fmt: off __slots__ = ( 'weights', + 'max_weight', ) # fmt: on - def __init__(self, children: List[Item]): + def __init__(self, children: List[Item], container: bool): self.weights: List[int] = [0, 0, 0, 0, 0] + self.max_weight: int = 5 if container is False else 10 key = lambda i: sys.maxsize if i.row is None else i.row children = sorted(children, key=key) @@ -111,7 +111,7 @@ def __init__(self, children: List[Item]): def find_open_space(self, item: Item) -> int: for index, weight in enumerate(self.weights): - if weight + item.width <= 5: + if weight + item.width <= self.max_weight: return index raise ValueError('could not find open space for item') @@ -119,8 +119,8 @@ def find_open_space(self, item: Item) -> int: def add_item(self, item: Item) -> None: if item.row is not None: total = self.weights[item.row] + item.width - if total > 5: - raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') + if total > 10: + raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)') self.weights[item.row] = total item._rendered_row = item.row else: @@ -195,7 +195,7 @@ def _init_children(self) -> List[Item[Self]]: def __init__(self, *, timeout: Optional[float] = 180.0): self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self._children) + self.__weights = _ViewWeights(self._children, self.__discord_ui_container__) self.id: str = os.urandom(16).hex() self._cache_key: Optional[int] = None self.__cancel_callback: Optional[Callable[[View], None]] = None @@ -228,23 +228,32 @@ def is_dispatchable(self) -> bool: # or not, this simply is, whether a view has a component other than a url button return any(item.is_dispatchable() for item in self.children) - def to_components(self) -> List[Dict[str, Any]]: - def key(item: Item) -> int: - return item._rendered_row or 0 + def has_components_v2(self) -> bool: + return any(c._is_v2() for c in self.children) - children = sorted(self._children, key=key) + def to_components(self) -> List[Dict[str, Any]]: components: List[Dict[str, Any]] = [] - for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] - if not children: - continue + rows_index: Dict[int, int] = {} + # helper mapping to find action rows for items that are not + # v2 components - components.append( - { - 'type': 1, - 'components': children, - } - ) + for child in self._children: + if child._is_v2(): + components.append(child.to_component_dict()) + else: + row = child._rendered_row or 0 + index = rows_index.get(row) + + if index is not None: + components[index]['components'].append(child) + else: + components.append( + { + 'type': 1, + 'components': [child.to_component_dict()], + }, + ) + rows_index[row] = len(components) - 1 return components From 86897182ba406936470467230c7db5cc93e9635d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 13:48:40 +0100 Subject: [PATCH 210/354] chore: more things to components v2 --- discord/http.py | 2 +- discord/ui/container.py | 46 ++++++++++++++++++-------------- discord/ui/section.py | 16 ++++++------ discord/ui/view.py | 55 +++++++++++++++++++++++++++++++-------- discord/webhook/async_.py | 7 +++++ 5 files changed, 86 insertions(+), 40 deletions(-) diff --git a/discord/http.py b/discord/http.py index 58b50172234e..d8eedeb2e296 100644 --- a/discord/http.py +++ b/discord/http.py @@ -57,6 +57,7 @@ from .mentions import AllowedMentions from . import __version__, utils from .utils import MISSING +from .flags import MessageFlags _log = logging.getLogger(__name__) @@ -66,7 +67,6 @@ from .ui.view import View from .embeds import Embed from .message import Attachment - from .flags import MessageFlags from .poll import Poll from .types import ( diff --git a/discord/ui/container.py b/discord/ui/container.py index 4bd68b724496..a2ca83a25390 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,16 +23,16 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar +import sys +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union from .item import Item +from .view import View, _component_to_item from ..enums import ComponentType if TYPE_CHECKING: from typing_extensions import Self - from .view import View - from ..colour import Colour, Color from ..components import Container as ContainerComponent @@ -41,15 +41,16 @@ __all__ = ('Container',) -class Container(Item[V]): +class Container(View, Item[V]): """Represents a Components V2 Container. .. versionadded:: 2.6 Parameters ---------- - children: List[:class:`Item`] - The initial children of this container. + children: List[Union[:class:`Item`, :class:`View`]] + The initial children or :class:`View`s of this container. Can have up to 10 + items. accent_colour: Optional[:class:`~discord.Colour`] The colour of the container. Defaults to ``None``. accent_color: Optional[:class:`~discord.Color`] @@ -57,31 +58,34 @@ class Container(Item[V]): spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults to ``False``. - timeout: Optional[:class:`float`] - The timeout to set to this container items. Defaults to ``180``. """ __discord_ui_container__ = True def __init__( self, - children: List[Item[Any]], + children: List[Union[Item[Any], View]], *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, + timeout: Optional[float] = 180, ) -> None: - self._children: List[Item[Any]] = children + if len(children) > 10: + raise ValueError('maximum number of components exceeded') + self._children: List[Union[Item[Any], View]] = children self.spoiler: bool = spoiler self._colour = accent_colour or accent_color + super().__init__(timeout=timeout) + @property - def children(self) -> List[Item[Any]]: + def children(self) -> List[Union[Item[Any], View]]: """List[:class:`Item`]: The children of this container.""" return self._children.copy() @children.setter - def children(self, value: List[Item[Any]]) -> None: + def children(self, value: List[Union[Item[Any], View]]) -> None: self._children = value @property @@ -100,22 +104,24 @@ def accent_colour(self, value: Optional[Colour]) -> None: def type(self) -> Literal[ComponentType.container]: return ComponentType.container + @property + def _views(self) -> List[View]: + return [c for c in self._children if isinstance(c, View)] + def _is_v2(self) -> bool: return True - def to_component_dict(self) -> Dict[str, Any]: - base = { + def to_components(self) -> List[Dict[str, Any]]: + components = super().to_components() + return [{ 'type': self.type.value, + 'accent_color': self._colour.value if self._colour else None, 'spoiler': self.spoiler, - 'components': [c.to_component_dict() for c in self._children] - } - if self._colour is not None: - base['accent_color'] = self._colour.value - return base + 'components': components, + }] @classmethod def from_component(cls, component: ContainerComponent) -> Self: - from .view import _component_to_item return cls( children=[_component_to_item(c) for c in component.children], accent_colour=component.accent_colour, diff --git a/discord/ui/section.py b/discord/ui/section.py index 81a0e4ba4fc0..5176d761bdb2 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -58,14 +58,14 @@ class Section(Item[V]): def __init__( self, - children: List[Union[TextDisplay[Any], str]], + children: List[Union[Item[Any], str]], *, accessory: Optional[Item[Any]] = None, ) -> None: if len(children) > 3: raise ValueError('maximum number of children exceeded') - self._children: List[TextDisplay[Any]] = [ - c if isinstance(c, TextDisplay) else TextDisplay(c) for c in children + self._children: List[Item[Any]] = [ + c if isinstance(c, Item) else TextDisplay(c) for c in children ] self.accessory: Optional[Item[Any]] = accessory @@ -76,7 +76,7 @@ def type(self) -> Literal[ComponentType.section]: def _is_v2(self) -> bool: return True - def add_item(self, item: Union[str, TextDisplay[Any]]) -> Self: + def add_item(self, item: Union[str, Item[Any]]) -> Self: """Adds an item to this section. This function returns the class instance to allow for fluent-style @@ -98,15 +98,15 @@ def add_item(self, item: Union[str, TextDisplay[Any]]) -> Self: if len(self._children) >= 3: raise ValueError('maximum number of children exceeded') - if not isinstance(item, (TextDisplay, str)): - raise TypeError(f'expected TextDisplay or str not {item.__class__.__name__}') + if not isinstance(item, (Item, str)): + raise TypeError(f'expected Item or str not {item.__class__.__name__}') self._children.append( - item if isinstance(item, TextDisplay) else TextDisplay(item), + item if isinstance(item, Item) else TextDisplay(item), ) return self - def remove_item(self, item: TextDisplay[Any]) -> Self: + def remove_item(self, item: Item[Any]) -> Self: """Removes an item from this section. This function returns the class instance to allow for fluent-style diff --git a/discord/ui/view.py b/discord/ui/view.py index 4abac51161f6..19bc3f33be20 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type +from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, Union from functools import partial from itertools import groupby @@ -95,13 +95,11 @@ class _ViewWeights: # fmt: off __slots__ = ( 'weights', - 'max_weight', ) # fmt: on - def __init__(self, children: List[Item], container: bool): + def __init__(self, children: List[Item]): self.weights: List[int] = [0, 0, 0, 0, 0] - self.max_weight: int = 5 if container is False else 10 key = lambda i: sys.maxsize if i.row is None else i.row children = sorted(children, key=key) @@ -109,18 +107,26 @@ def __init__(self, children: List[Item], container: bool): for item in group: self.add_item(item) - def find_open_space(self, item: Item) -> int: + def find_open_space(self, item: Union[Item, View]) -> int: for index, weight in enumerate(self.weights): - if weight + item.width <= self.max_weight: + if weight + item.width <= 5: return index raise ValueError('could not find open space for item') - def add_item(self, item: Item) -> None: + def add_item(self, item: Union[Item, View]) -> None: + if hasattr(item, '__discord_ui_container__') and item.__discord_ui_container__ is True: + raise TypeError( + 'containers cannot be added to views' + ) + + if item._is_v2() and not self.v2_weights(): + # v2 components allow up to 10 rows + self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width if total > 10: - raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)') + raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') self.weights[item.row] = total item._rendered_row = item.row else: @@ -128,7 +134,7 @@ def add_item(self, item: Item) -> None: self.weights[index] += item.width item._rendered_row = index - def remove_item(self, item: Item) -> None: + def remove_item(self, item: Union[Item, View]) -> None: if item._rendered_row is not None: self.weights[item._rendered_row] -= item.width item._rendered_row = None @@ -136,6 +142,9 @@ def remove_item(self, item: Item) -> None: def clear(self) -> None: self.weights = [0, 0, 0, 0, 0] + def v2_weights(self) -> bool: + return sum(1 if w > 0 else 0 for w in self.weights) > 5 + class _ViewCallback: __slots__ = ('view', 'callback', 'item') @@ -176,6 +185,8 @@ def __init_subclass__(cls) -> None: for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): children[name] = member + if cls.__discord_ui_container__ and isinstance(member, View): + children[name] = member if len(children) > 25: raise TypeError('View cannot have more than 25 children') @@ -192,16 +203,25 @@ def _init_children(self) -> List[Item[Self]]: children.append(item) return children - def __init__(self, *, timeout: Optional[float] = 180.0): + def __init__(self, *, timeout: Optional[float] = 180.0, row: Optional[int] = None): self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self._children, self.__discord_ui_container__) + self.__weights = _ViewWeights(self._children) self.id: str = os.urandom(16).hex() self._cache_key: Optional[int] = None self.__cancel_callback: Optional[Callable[[View], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self.row: Optional[int] = row + self._rendered_row: Optional[int] = None + + def _is_v2(self) -> bool: + return False + + @property + def width(self): + return 5 def __repr__(self) -> str: return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' @@ -602,6 +622,19 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False + # components V2 containers allow for views to exist inside them + # with dispatchable items, so we iterate over it and add it + # to the store + if hasattr(view, '_views'): + for v in view._views: + for item in v._children: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + self._dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item + is_fully_dynamic = False + view._cache_key = message_id if message_id is not None and not is_fully_dynamic: self._synced_message_views[message_id] = view diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 3b62b10faa2c..f1cfb573bb71 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -592,6 +592,13 @@ def interaction_message_response_params( if view is not MISSING: if view is not None: data['components'] = view.to_components() + + if view.has_components_v2(): + if flags is not MISSING: + flags.components_v2 = True + else: + flags = MessageFlags(components_v2=True) + else: data['components'] = [] From 76e202811831641aa0c8735686a435d1c842794a Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:44:49 +0100 Subject: [PATCH 211/354] chore: More v2 components on UI and idk some changes --- discord/abc.py | 13 ++++++- discord/http.py | 2 ++ discord/ui/button.py | 18 +++++----- discord/ui/container.py | 59 +++++++++++++++++++++++-------- discord/ui/section.py | 5 +++ discord/ui/text_display.py | 71 ++++++++++++++++++++++++++++++++++++++ discord/ui/view.py | 53 ++++++++++++---------------- 7 files changed, 167 insertions(+), 54 deletions(-) create mode 100644 discord/ui/text_display.py diff --git a/discord/abc.py b/discord/abc.py index 70531fb2005e..1380b30488fb 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1389,6 +1389,7 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., + views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1410,6 +1411,7 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., + views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1431,6 +1433,7 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., + views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1452,6 +1455,7 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., + views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1474,6 +1478,7 @@ async def send( reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, view: Optional[View] = None, + views: Optional[Sequence[View]] = None, suppress_embeds: bool = False, silent: bool = False, poll: Optional[Poll] = None, @@ -1550,6 +1555,10 @@ async def send( A Discord UI View to add to the message. .. versionadded:: 2.0 + views: Sequence[:class:`discord.ui.View`] + A sequence of Discord UI Views to add to the message. + + .. versionadded:: 2.6 stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. @@ -1580,7 +1589,8 @@ async def send( You specified both ``file`` and ``files``, or you specified both ``embed`` and ``embeds``, or the ``reference`` object is not a :class:`~discord.Message`, - :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`. + :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`, + or you specified both ``view`` and ``views``. Returns --------- @@ -1635,6 +1645,7 @@ async def send( mention_author=mention_author, stickers=sticker_ids, view=view, + views=views if views is not None else MISSING, flags=flags, poll=poll, ) as params: diff --git a/discord/http.py b/discord/http.py index d8eedeb2e296..c6e4d1377277 100644 --- a/discord/http.py +++ b/discord/http.py @@ -192,6 +192,8 @@ def handle_message_parameters( if view is not MISSING: if view is not None: + if getattr(view, '__discord_ui_container__', False): + raise TypeError('Containers must be wrapped around Views') payload['components'] = view.to_components() if view.has_components_v2(): diff --git a/discord/ui/button.py b/discord/ui/button.py index 43bd3a8b0f9d..b4df36aed8a9 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -73,10 +73,11 @@ class Button(Item[V]): The emoji of the button, if available. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 - rows. By default, items are arranged automatically into those 5 rows. If you'd - like to control the relative positioning of the row then passing an index is advised. - For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 4 (i.e. zero indexed). + rows in a :class:`View`, but up to 10 on a :class:`Container`. By default, + items are arranged automatically into those rows. If you'd like to control the + relative positioning of the row then passing an index is advised. For example, + row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed). sku_id: Optional[:class:`int`] The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` nor ``custom_id``. @@ -304,10 +305,11 @@ def button( or a full :class:`.Emoji`. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 - rows. By default, items are arranged automatically into those 5 rows. If you'd - like to control the relative positioning of the row then passing an index is advised. - For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 4 (i.e. zero indexed). + rows in a :class:`View`, but up to 10 on a :class:`Container`. By default, + items are arranged automatically into those rows. If you'd like to control the + relative positioning of the row then passing an index is advised. For example, + row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed). """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: diff --git a/discord/ui/container.py b/discord/ui/container.py index a2ca83a25390..a98b0d965ab5 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,11 +23,11 @@ """ from __future__ import annotations -import sys -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar from .item import Item from .view import View, _component_to_item +from .dynamic import DynamicItem from ..enums import ComponentType if TYPE_CHECKING: @@ -48,7 +48,7 @@ class Container(View, Item[V]): Parameters ---------- - children: List[Union[:class:`Item`, :class:`View`]] + children: List[:class:`Item`] The initial children or :class:`View`s of this container. Can have up to 10 items. accent_colour: Optional[:class:`~discord.Colour`] @@ -58,34 +58,47 @@ class Container(View, Item[V]): spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults to ``False``. + timeout: Optional[:class:`float`] + Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. """ __discord_ui_container__ = True def __init__( self, - children: List[Union[Item[Any], View]], + children: List[Item[Any]], *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, timeout: Optional[float] = 180, + row: Optional[int] = None, ) -> None: - if len(children) > 10: + super().__init__(timeout=timeout) + if len(children) + len(self._children) > 10: raise ValueError('maximum number of components exceeded') - self._children: List[Union[Item[Any], View]] = children + self._children.extend(children) self.spoiler: bool = spoiler self._colour = accent_colour or accent_color - super().__init__(timeout=timeout) + self._view: Optional[V] = None + self._row: Optional[int] = None + self._rendered_row: Optional[int] = None + self.row: Optional[int] = row + + def _init_children(self) -> List[Item[Self]]: + if self.__weights.max_weight != 10: + self.__weights.max_weight = 10 + return super()._init_children() @property - def children(self) -> List[Union[Item[Any], View]]: + def children(self) -> List[Item[Self]]: """List[:class:`Item`]: The children of this container.""" return self._children.copy() @children.setter - def children(self, value: List[Union[Item[Any], View]]) -> None: + def children(self, value: List[Item[Any]]) -> None: self._children = value @property @@ -105,20 +118,38 @@ def type(self) -> Literal[ComponentType.container]: return ComponentType.container @property - def _views(self) -> List[View]: - return [c for c in self._children if isinstance(c, View)] + def width(self): + return 5 def _is_v2(self) -> bool: return True - def to_components(self) -> List[Dict[str, Any]]: + def is_dispatchable(self) -> bool: + return any(c.is_dispatchable() for c in self.children) + + def to_component_dict(self) -> Dict[str, Any]: components = super().to_components() - return [{ + return { 'type': self.type.value, 'accent_color': self._colour.value if self._colour else None, 'spoiler': self.spoiler, 'components': components, - }] + } + + def _update_store_data( + self, + dispatch_info: Dict[Tuple[int, str], Item[Any]], + dynamic_items: Dict[Any, Type[DynamicItem]], + ) -> bool: + is_fully_dynamic = True + for item in self._children: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + is_fully_dynamic = False + return is_fully_dynamic @classmethod def from_component(cls, component: ContainerComponent) -> Self: diff --git a/discord/ui/section.py b/discord/ui/section.py index 5176d761bdb2..0012d0118186 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -62,6 +62,7 @@ def __init__( *, accessory: Optional[Item[Any]] = None, ) -> None: + super().__init__() if len(children) > 3: raise ValueError('maximum number of children exceeded') self._children: List[Item[Any]] = [ @@ -73,6 +74,10 @@ def __init__( def type(self) -> Literal[ComponentType.section]: return ComponentType.section + @property + def width(self): + return 5 + def _is_v2(self) -> bool: return True diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py new file mode 100644 index 000000000000..0daff9c89e5c --- /dev/null +++ b/discord/ui/text_display.py @@ -0,0 +1,71 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, TypeVar + +from .item import Item +from ..components import TextDisplay as TextDisplayComponent +from ..enums import ComponentType + +if TYPE_CHECKING: + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('TextDisplay',) + + +class TextDisplay(Item[V]): + """Represents a UI text display. + + .. versionadded:: 2.6 + + Parameters + ---------- + content: :class:`str` + The content of this text display. + """ + + def __init__(self, content: str) -> None: + super().__init__() + self.content: str = content + + self._underlying = TextDisplayComponent._raw_construct( + content=content, + ) + + def to_component_dict(self): + return self._underlying.to_dict() + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.text_display]: + return self._underlying.type + + def _is_v2(self) -> bool: + return True diff --git a/discord/ui/view.py b/discord/ui/view.py index 19bc3f33be20..4afcd9fad0ce 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -95,11 +95,13 @@ class _ViewWeights: # fmt: off __slots__ = ( 'weights', + 'max_weight', ) # fmt: on def __init__(self, children: List[Item]): self.weights: List[int] = [0, 0, 0, 0, 0] + self.max_weight: int = 5 key = lambda i: sys.maxsize if i.row is None else i.row children = sorted(children, key=key) @@ -107,26 +109,21 @@ def __init__(self, children: List[Item]): for item in group: self.add_item(item) - def find_open_space(self, item: Union[Item, View]) -> int: + def find_open_space(self, item: Item) -> int: for index, weight in enumerate(self.weights): if weight + item.width <= 5: return index raise ValueError('could not find open space for item') - def add_item(self, item: Union[Item, View]) -> None: - if hasattr(item, '__discord_ui_container__') and item.__discord_ui_container__ is True: - raise TypeError( - 'containers cannot be added to views' - ) - + def add_item(self, item: Item) -> None: if item._is_v2() and not self.v2_weights(): # v2 components allow up to 10 rows self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width - if total > 10: - raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') + if total > self.max_weight: + raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)') self.weights[item.row] = total item._rendered_row = item.row else: @@ -134,7 +131,7 @@ def add_item(self, item: Union[Item, View]) -> None: self.weights[index] += item.width item._rendered_row = index - def remove_item(self, item: Union[Item, View]) -> None: + def remove_item(self, item: Item) -> None: if item._rendered_row is not None: self.weights[item._rendered_row] -= item.width item._rendered_row = None @@ -185,8 +182,6 @@ def __init_subclass__(cls) -> None: for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): children[name] = member - if cls.__discord_ui_container__ and isinstance(member, View): - children[name] = member if len(children) > 25: raise TypeError('View cannot have more than 25 children') @@ -203,7 +198,7 @@ def _init_children(self) -> List[Item[Self]]: children.append(item) return children - def __init__(self, *, timeout: Optional[float] = 180.0, row: Optional[int] = None): + def __init__(self, *, timeout: Optional[float] = 180.0): self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() self.__weights = _ViewWeights(self._children) @@ -213,8 +208,6 @@ def __init__(self, *, timeout: Optional[float] = 180.0, row: Optional[int] = Non self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() - self.row: Optional[int] = row - self._rendered_row: Optional[int] = None def _is_v2(self) -> bool: return False @@ -257,7 +250,12 @@ def to_components(self) -> List[Dict[str, Any]]: # helper mapping to find action rows for items that are not # v2 components - for child in self._children: + def key(item: Item) -> int: + return item._rendered_row or 0 + + # instead of grouping by row we will sort it so it is added + # in order and should work as the original implementation + for child in sorted(self._children, key=key): if child._is_v2(): components.append(child.to_component_dict()) else: @@ -619,21 +617,14 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: pattern = item.__discord_ui_compiled_template__ self._dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore - is_fully_dynamic = False - - # components V2 containers allow for views to exist inside them - # with dispatchable items, so we iterate over it and add it - # to the store - if hasattr(view, '_views'): - for v in view._views: - for item in v._children: - if isinstance(item, DynamicItem): - pattern = item.__discord_ui_compiled_template__ - self._dynamic_items[pattern] = item.__class__ - elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item - is_fully_dynamic = False + if getattr(item, '__discord_ui_container__', False): + is_fully_dynamic = item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) + else: + dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + is_fully_dynamic = False view._cache_key = message_id if message_id is not None and not is_fully_dynamic: From b872925d9ffe2b375acc557dd55534ae0d143bba Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:05:26 +0100 Subject: [PATCH 212/354] chore: Remove views things --- discord/abc.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 1380b30488fb..ae6a1fb15811 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1389,7 +1389,6 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., - views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1411,7 +1410,6 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., - views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1433,7 +1431,6 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., - views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1455,7 +1452,6 @@ async def send( reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., view: View = ..., - views: Sequence[View] = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1478,7 +1474,6 @@ async def send( reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, view: Optional[View] = None, - views: Optional[Sequence[View]] = None, suppress_embeds: bool = False, silent: bool = False, poll: Optional[Poll] = None, @@ -1555,10 +1550,6 @@ async def send( A Discord UI View to add to the message. .. versionadded:: 2.0 - views: Sequence[:class:`discord.ui.View`] - A sequence of Discord UI Views to add to the message. - - .. versionadded:: 2.6 stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. @@ -1645,7 +1636,6 @@ async def send( mention_author=mention_author, stickers=sticker_ids, view=view, - views=views if views is not None else MISSING, flags=flags, poll=poll, ) as params: From 4467ebaca690c4d29d4cdc3d5177c1b52c1e8d8e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:10:00 +0100 Subject: [PATCH 213/354] chore: Donnot subclass AttachmentBase and fake payload for Attachment initialization on UnfurledAttachment --- discord/attachment.py | 221 ++++++++++++++++++++---------------------- 1 file changed, 104 insertions(+), 117 deletions(-) diff --git a/discord/attachment.py b/discord/attachment.py index 195ce30b5d3c..4b9765b99fd8 100644 --- a/discord/attachment.py +++ b/discord/attachment.py @@ -52,9 +52,87 @@ ) -class AttachmentBase: +class Attachment(Hashable): + """Represents an attachment from Discord. + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the attachment. + + .. describe:: x == y + + Checks if the attachment is equal to another attachment. + + .. describe:: x != y + + Checks if the attachment is not equal to another attachment. + + .. describe:: hash(x) + + Returns the hash of the attachment. + + .. versionchanged:: 1.7 + Attachment can now be casted to :class:`str` and is hashable. + + Attributes + ------------ + id: :class:`int` + The attachment ID. + size: :class:`int` + The attachment size in bytes. + height: Optional[:class:`int`] + The attachment's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The attachment's width, in pixels. Only applicable to images and videos. + filename: :class:`str` + The attachment's filename. + url: :class:`str` + The attachment URL. If the message this attachment was attached + to is deleted, then this will 404. + proxy_url: :class:`str` + The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the + case of images. When the message is deleted, this URL might be valid for a few + minutes or not valid at all. + content_type: Optional[:class:`str`] + The attachment's `media type `_ + + .. versionadded:: 1.7 + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + + .. versionadded:: 2.0 + ephemeral: :class:`bool` + Whether the attachment is ephemeral. + + .. versionadded:: 2.0 + duration: Optional[:class:`float`] + The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + waveform: Optional[:class:`bytes`] + The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 + spoiler: :class:`bool` + Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned + data. + + .. versionadded:: 2.6 + """ __slots__ = ( + 'id', + 'size', + 'ephemeral', + 'duration', + 'waveform', + 'title', 'url', 'proxy_url', 'description', @@ -68,7 +146,13 @@ class AttachmentBase: '_state', ) - def __init__(self, data: AttachmentBasePayload, state: Optional[ConnectionState]) -> None: + def __init__(self, *, data: AttachmentPayload, state: Optional[ConnectionState]): + self.id: int = int(data['id']) + self.filename: str = data['filename'] + self.size: int = data['size'] + self.ephemeral: bool = data.get('ephemeral', False) + self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') self._state: Optional[ConnectionState] = state self._http: Optional[HTTPClient] = state.http if state else None self.url: str = data['url'] @@ -248,11 +332,25 @@ async def to_file( spoiler=spoiler, ) - def to_dict(self) -> AttachmentBasePayload: - base: AttachmentBasePayload = { + def is_spoiler(self) -> bool: + """:class:`bool`: Whether this attachment contains a spoiler.""" + return self.spoiler or self.filename.startswith('SPOILER_') + + def is_voice_message(self) -> bool: + """:class:`bool`: Whether this attachment is a voice message.""" + return self.duration is not None and 'voice-message' in self.url + + def __repr__(self) -> str: + return f'' + + def to_dict(self) -> AttachmentPayload: + base: AttachmentPayload = { 'url': self.url, 'proxy_url': self.proxy_url, 'spoiler': self.spoiler, + 'id': self.id, + 'filename': self.filename, + 'size': self.size, } if self.width: @@ -265,118 +363,7 @@ def to_dict(self) -> AttachmentBasePayload: return base -class Attachment(Hashable, AttachmentBase): - """Represents an attachment from Discord. - - .. container:: operations - - .. describe:: str(x) - - Returns the URL of the attachment. - - .. describe:: x == y - - Checks if the attachment is equal to another attachment. - - .. describe:: x != y - - Checks if the attachment is not equal to another attachment. - - .. describe:: hash(x) - - Returns the hash of the attachment. - - .. versionchanged:: 1.7 - Attachment can now be casted to :class:`str` and is hashable. - - Attributes - ------------ - id: :class:`int` - The attachment ID. - size: :class:`int` - The attachment size in bytes. - height: Optional[:class:`int`] - The attachment's height, in pixels. Only applicable to images and videos. - width: Optional[:class:`int`] - The attachment's width, in pixels. Only applicable to images and videos. - filename: :class:`str` - The attachment's filename. - url: :class:`str` - The attachment URL. If the message this attachment was attached - to is deleted, then this will 404. - proxy_url: :class:`str` - The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the - case of images. When the message is deleted, this URL might be valid for a few - minutes or not valid at all. - content_type: Optional[:class:`str`] - The attachment's `media type `_ - - .. versionadded:: 1.7 - description: Optional[:class:`str`] - The attachment's description. Only applicable to images. - - .. versionadded:: 2.0 - ephemeral: :class:`bool` - Whether the attachment is ephemeral. - - .. versionadded:: 2.0 - duration: Optional[:class:`float`] - The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - waveform: Optional[:class:`bytes`] - The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - title: Optional[:class:`str`] - The normalised version of the attachment's filename. - - .. versionadded:: 2.5 - spoiler: :class:`bool` - Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned - data. - - .. versionadded:: 2.6 - """ - - __slots__ = ( - 'id', - 'size', - 'ephemeral', - 'duration', - 'waveform', - 'title', - ) - - def __init__(self, *, data: AttachmentPayload, state: ConnectionState): - self.id: int = int(data['id']) - self.filename: str = data['filename'] - self.size: int = data['size'] - self.ephemeral: bool = data.get('ephemeral', False) - self.duration: Optional[float] = data.get('duration_secs') - self.title: Optional[str] = data.get('title') - super().__init__(data, state) - - def is_spoiler(self) -> bool: - """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.spoiler or self.filename.startswith('SPOILER_') - - def is_voice_message(self) -> bool: - """:class:`bool`: Whether this attachment is a voice message.""" - return self.duration is not None and 'voice-message' in self.url - - def __repr__(self) -> str: - return f'' - - def to_dict(self) -> AttachmentPayload: - result: AttachmentPayload = super().to_dict() # pyright: ignore[reportAssignmentType] - result['id'] = self.id - result['filename'] = self.filename - result['size'] = self.size - return result - - -class UnfurledAttachment(AttachmentBase): +class UnfurledAttachment(Attachment): """Represents an unfurled attachment item from a :class:`Component`. .. versionadded:: 2.6 @@ -425,7 +412,7 @@ class UnfurledAttachment(AttachmentBase): def __init__(self, data: UnfurledAttachmentPayload, state: Optional[ConnectionState]) -> None: self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data.get('loading_state', 0)) - super().__init__(data, state) + super().__init__(data={'id': 0, 'filename': '', 'size': 0, **data}, state=state) # type: ignore def __repr__(self) -> str: return f'' From 4aef97e24905d79f5184c1952660b3c3d43e3a31 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:47:22 +0100 Subject: [PATCH 214/354] chore: undo attachment file move --- discord/attachment.py | 421 ---------------------------------------- discord/components.py | 116 +++++++++-- discord/enums.py | 4 +- discord/ui/thumbnail.py | 19 +- discord/ui/view.py | 3 + 5 files changed, 115 insertions(+), 448 deletions(-) delete mode 100644 discord/attachment.py diff --git a/discord/attachment.py b/discord/attachment.py deleted file mode 100644 index 4b9765b99fd8..000000000000 --- a/discord/attachment.py +++ /dev/null @@ -1,421 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-present Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the 'Software'), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" -from __future__ import annotations - -import io -from os import PathLike -from typing import TYPE_CHECKING, Any, Optional, Union - -from .errors import ClientException -from .mixins import Hashable -from .file import File -from .flags import AttachmentFlags -from .enums import MediaLoadingState, try_enum -from . import utils - -if TYPE_CHECKING: - from .types.attachment import ( - AttachmentBase as AttachmentBasePayload, - Attachment as AttachmentPayload, - UnfurledAttachment as UnfurledAttachmentPayload, - ) - - from .http import HTTPClient - from .state import ConnectionState - -MISSING = utils.MISSING - -__all__ = ( - 'Attachment', - 'UnfurledAttachment', -) - - -class Attachment(Hashable): - """Represents an attachment from Discord. - - .. container:: operations - - .. describe:: str(x) - - Returns the URL of the attachment. - - .. describe:: x == y - - Checks if the attachment is equal to another attachment. - - .. describe:: x != y - - Checks if the attachment is not equal to another attachment. - - .. describe:: hash(x) - - Returns the hash of the attachment. - - .. versionchanged:: 1.7 - Attachment can now be casted to :class:`str` and is hashable. - - Attributes - ------------ - id: :class:`int` - The attachment ID. - size: :class:`int` - The attachment size in bytes. - height: Optional[:class:`int`] - The attachment's height, in pixels. Only applicable to images and videos. - width: Optional[:class:`int`] - The attachment's width, in pixels. Only applicable to images and videos. - filename: :class:`str` - The attachment's filename. - url: :class:`str` - The attachment URL. If the message this attachment was attached - to is deleted, then this will 404. - proxy_url: :class:`str` - The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the - case of images. When the message is deleted, this URL might be valid for a few - minutes or not valid at all. - content_type: Optional[:class:`str`] - The attachment's `media type `_ - - .. versionadded:: 1.7 - description: Optional[:class:`str`] - The attachment's description. Only applicable to images. - - .. versionadded:: 2.0 - ephemeral: :class:`bool` - Whether the attachment is ephemeral. - - .. versionadded:: 2.0 - duration: Optional[:class:`float`] - The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - waveform: Optional[:class:`bytes`] - The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. - - .. versionadded:: 2.3 - title: Optional[:class:`str`] - The normalised version of the attachment's filename. - - .. versionadded:: 2.5 - spoiler: :class:`bool` - Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned - data. - - .. versionadded:: 2.6 - """ - - __slots__ = ( - 'id', - 'size', - 'ephemeral', - 'duration', - 'waveform', - 'title', - 'url', - 'proxy_url', - 'description', - 'filename', - 'spoiler', - 'height', - 'width', - 'content_type', - '_flags', - '_http', - '_state', - ) - - def __init__(self, *, data: AttachmentPayload, state: Optional[ConnectionState]): - self.id: int = int(data['id']) - self.filename: str = data['filename'] - self.size: int = data['size'] - self.ephemeral: bool = data.get('ephemeral', False) - self.duration: Optional[float] = data.get('duration_secs') - self.title: Optional[str] = data.get('title') - self._state: Optional[ConnectionState] = state - self._http: Optional[HTTPClient] = state.http if state else None - self.url: str = data['url'] - self.proxy_url: str = data['proxy_url'] - self.description: Optional[str] = data.get('description') - self.spoiler: bool = data.get('spoiler', False) - self.height: Optional[int] = data.get('height') - self.width: Optional[int] = data.get('width') - self.content_type: Optional[str] = data.get('content_type') - self._flags: int = data.get('flags', 0) - - @property - def flags(self) -> AttachmentFlags: - """:class:`AttachmentFlags`: The attachment's flag value.""" - return AttachmentFlags._from_value(self._flags) - - def __str__(self) -> str: - return self.url or '' - - async def save( - self, - fp: Union[io.BufferedIOBase, PathLike[Any]], - *, - seek_begin: bool = True, - use_cached: bool = False, - ) -> int: - """|coro| - - Saves this attachment into a file-like object. - - Parameters - ---------- - fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`] - The file-like object to save this attachment to or the filename - to use. If a filename is passed then a file is created with that - filename and used instead. - seek_begin: :class:`bool` - Whether to seek to the beginning of the file after saving is - successfully done. - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - Raises - -------- - HTTPException - Saving the attachment failed. - NotFound - The attachment was deleted. - - Returns - -------- - :class:`int` - The number of bytes written. - """ - data = await self.read(use_cached=use_cached) - if isinstance(fp, io.BufferedIOBase): - written = fp.write(data) - if seek_begin: - fp.seek(0) - return written - else: - with open(fp, 'wb') as f: - return f.write(data) - - async def read(self, *, use_cached: bool = False) -> bytes: - """|coro| - - Retrieves the content of this attachment as a :class:`bytes` object. - - .. versionadded:: 1.1 - - Parameters - ----------- - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - Raises - ------ - HTTPException - Downloading the attachment failed. - Forbidden - You do not have permissions to access this attachment - NotFound - The attachment was deleted. - ClientException - Cannot read a stateless attachment. - - Returns - ------- - :class:`bytes` - The contents of the attachment. - """ - if not self._http: - raise ClientException( - 'Cannot read a stateless attachment' - ) - - url = self.proxy_url if use_cached else self.url - data = await self._http.get_from_cdn(url) - return data - - async def to_file( - self, - *, - filename: Optional[str] = MISSING, - description: Optional[str] = MISSING, - use_cached: bool = False, - spoiler: bool = False, - ) -> File: - """|coro| - - Converts the attachment into a :class:`File` suitable for sending via - :meth:`abc.Messageable.send`. - - .. versionadded:: 1.3 - - Parameters - ----------- - filename: Optional[:class:`str`] - The filename to use for the file. If not specified then the filename - of the attachment is used instead. - - .. versionadded:: 2.0 - description: Optional[:class:`str`] - The description to use for the file. If not specified then the - description of the attachment is used instead. - - .. versionadded:: 2.0 - use_cached: :class:`bool` - Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading - the attachment. This will allow attachments to be saved after deletion - more often, compared to the regular URL which is generally deleted right - after the message is deleted. Note that this can still fail to download - deleted attachments if too much time has passed and it does not work - on some types of attachments. - - .. versionadded:: 1.4 - spoiler: :class:`bool` - Whether the file is a spoiler. - - .. versionadded:: 1.4 - - Raises - ------ - HTTPException - Downloading the attachment failed. - Forbidden - You do not have permissions to access this attachment - NotFound - The attachment was deleted. - - Returns - ------- - :class:`File` - The attachment as a file suitable for sending. - """ - - data = await self.read(use_cached=use_cached) - file_filename = filename if filename is not MISSING else self.filename - file_description = ( - description if description is not MISSING else self.description - ) - return File( - io.BytesIO(data), - filename=file_filename, - description=file_description, - spoiler=spoiler, - ) - - def is_spoiler(self) -> bool: - """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.spoiler or self.filename.startswith('SPOILER_') - - def is_voice_message(self) -> bool: - """:class:`bool`: Whether this attachment is a voice message.""" - return self.duration is not None and 'voice-message' in self.url - - def __repr__(self) -> str: - return f'' - - def to_dict(self) -> AttachmentPayload: - base: AttachmentPayload = { - 'url': self.url, - 'proxy_url': self.proxy_url, - 'spoiler': self.spoiler, - 'id': self.id, - 'filename': self.filename, - 'size': self.size, - } - - if self.width: - base['width'] = self.width - if self.height: - base['height'] = self.height - if self.description: - base['description'] = self.description - - return base - - -class UnfurledAttachment(Attachment): - """Represents an unfurled attachment item from a :class:`Component`. - - .. versionadded:: 2.6 - - .. container:: operations - - .. describe:: str(x) - - Returns the URL of the attachment. - - .. describe:: x == y - - Checks if the unfurled attachment is equal to another unfurled attachment. - - .. describe:: x != y - - Checks if the unfurled attachment is not equal to another unfurled attachment. - - Attributes - ---------- - height: Optional[:class:`int`] - The attachment's height, in pixels. Only applicable to images and videos. - width: Optional[:class:`int`] - The attachment's width, in pixels. Only applicable to images and videos. - url: :class:`str` - The attachment URL. If the message this attachment was attached - to is deleted, then this will 404. - proxy_url: :class:`str` - The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the - case of images. When the message is deleted, this URL might be valid for a few - minutes or not valid at all. - content_type: Optional[:class:`str`] - The attachment's `media type `_ - description: Optional[:class:`str`] - The attachment's description. Only applicable to images. - spoiler: :class:`bool` - Whether the attachment is a spoiler or not. Unlike :meth:`.is_spoiler`, this uses the API returned - data. - loading_state: :class:`MediaLoadingState` - The cache state of this unfurled attachment. - """ - - __slots__ = ( - 'loading_state', - ) - - def __init__(self, data: UnfurledAttachmentPayload, state: Optional[ConnectionState]) -> None: - self.loading_state: MediaLoadingState = try_enum(MediaLoadingState, data.get('loading_state', 0)) - super().__init__(data={'id': 0, 'filename': '', 'size': 0, **data}, state=state) # type: ignore - - def __repr__(self) -> str: - return f'' - - def to_object_dict(self): - return {'url': self.url} diff --git a/discord/components.py b/discord/components.py index 4e0196f7dd92..7330b82e9448 100644 --- a/discord/components.py +++ b/discord/components.py @@ -34,7 +34,7 @@ Union, ) -from .attachment import UnfurledAttachment +from .asset import AssetMixin from .enums import ( try_enum, ComponentType, @@ -43,7 +43,9 @@ ChannelType, SelectDefaultValueType, SeparatorSize, + MediaItemLoadingState, ) +from .flags import AttachmentFlags from .colour import Colour from .utils import get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag @@ -68,6 +70,7 @@ MediaGalleryItem as MediaGalleryItemPayload, ThumbnailComponent as ThumbnailComponentPayload, ContainerComponent as ContainerComponentPayload, + UnfurledMediaItem as UnfurledMediaItemPayload, ) from .emoji import Emoji @@ -773,7 +776,7 @@ def __init__( data: ThumbnailComponentPayload, state: Optional[ConnectionState], ) -> None: - self.media: UnfurledAttachment = UnfurledAttachment(data['media'], state) + self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state) self.description: Optional[str] = data.get('description') self.spoiler: bool = data.get('spoiler', False) @@ -817,15 +820,96 @@ def to_dict(self) -> TextComponentPayload: } +class UnfurledMediaItem(AssetMixin): + """Represents an unfurled media item that can be used on + :class:`MediaGalleryItem`s. + + Unlike :class:`UnfurledAttachment` this represents a media item + not yet stored on Discord and thus it does not have any data. + + Parameters + ---------- + url: :class:`str` + The URL of this media item. + + Attributes + ---------- + proxy_url: Optional[:class:`str`] + The proxy URL. This is a cached version of the :attr:`~UnfurledMediaItem.url` in the + case of images. When the message is deleted, this URL might be valid for a few minutes + or not valid at all. + height: Optional[:class:`int`] + The media item's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The media item's width, in pixels. Only applicable to images and videos. + content_type: Optional[:class:`str`] + The media item's `media type `_ + placeholder: Optional[:class:`str`] + The media item's placeholder. + loading_state: Optional[:class:`MediaItemLoadingState`] + The loading state of this media item. + """ + + __slots__ = ( + 'url', + 'proxy_url', + 'height', + 'width', + 'content_type', + '_flags', + 'placeholder', + 'loading_state', + '_state', + ) + + def __init__(self, url: str) -> None: + self.url: str = url + + self.proxy_url: Optional[str] = None + self.height: Optional[int] = None + self.width: Optional[int] = None + self.content_type: Optional[str] = None + self._flags: int = 0 + self.placeholder: Optional[str] = None + self.loading_state: Optional[MediaItemLoadingState] = None + self._state: Optional[ConnectionState] = None + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: This media item's flags.""" + return AttachmentFlags._from_value(self._flags) + + @classmethod + def _from_data(cls, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]): + self = cls(data['url']) + self._update(data, state) + return self + + def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionState]) -> None: + self.proxy_url = data['proxy_url'] + self.height = data.get('height') + self.width = data.get('width') + self.content_type = data.get('content_type') + self._flags = data.get('flags', 0) + self.placeholder = data.get('placeholder') + self.loading_state = try_enum(MediaItemLoadingState, data['loading_state']) + self._state = state + + def to_dict(self): + return { + 'url': self.url, + } + + class MediaGalleryItem: """Represents a :class:`MediaGalleryComponent` media item. Parameters ---------- - url: :class:`str` - The url of the media item. This can be a local file uploaded - as an attachment in the message, that can be accessed using - the ``attachment://file-name.extension`` format. + media: Union[:class:`str`, :class:`UnfurledMediaItem`] + The media item data. This can be a string representing a local + file uploaded as an attachment in the message, that can be accessed + using the ``attachment://file-name.extension`` format. description: Optional[:class:`str`] The description to show within this item. spoiler: :class:`bool` @@ -833,7 +917,7 @@ class MediaGalleryItem: """ __slots__ = ( - 'url', + 'media', 'description', 'spoiler', '_state', @@ -841,12 +925,12 @@ class MediaGalleryItem: def __init__( self, - url: str, + media: Union[str, UnfurledMediaItem], *, description: Optional[str] = None, spoiler: bool = False, ) -> None: - self.url: str = url + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media self.description: Optional[str] = description self.spoiler: bool = spoiler self._state: Optional[ConnectionState] = None @@ -857,7 +941,7 @@ def _from_data( ) -> MediaGalleryItem: media = data['media'] self = cls( - url=media['url'], + media=media['url'], description=data.get('description'), spoiler=data.get('spoiler', False), ) @@ -873,8 +957,8 @@ def _from_gallery( return [cls._from_data(item, state) for item in items] def to_dict(self) -> MediaGalleryItemPayload: - return { # type: ignore - 'media': {'url': self.url}, + return { + 'media': self.media.to_dict(), # type: ignore 'description': self.description, 'spoiler': self.spoiler, } @@ -927,9 +1011,7 @@ class FileComponent(Component): ) def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: - self.media: UnfurledAttachment = UnfurledAttachment( - data['file'], state, - ) + self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state) self.spoiler: bool = data.get('spoiler', False) @property @@ -937,8 +1019,8 @@ def type(self) -> Literal[ComponentType.file]: return ComponentType.file def to_dict(self) -> FileComponentPayload: - return { # type: ignore - 'file': {'url': self.url}, + return { + 'file': self.media.to_dict(), # type: ignore 'spoiler': self.spoiler, 'type': self.type.value, } diff --git a/discord/enums.py b/discord/enums.py index 025f0bf147c1..49684935f7c7 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -78,7 +78,7 @@ 'SubscriptionStatus', 'MessageReferenceType', 'SeparatorSize', - 'MediaLoadingState', + 'MediaItemLoadingState', ) @@ -877,7 +877,7 @@ class SeparatorSize(Enum): large = 2 -class MediaLoadingState(Enum): +class MediaItemLoadingState(Enum): unknown = 0 loading = 1 loaded = 2 diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index a984a1892f9c..67e380f65c5c 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -23,10 +23,11 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, TypeVar, Union from .item import Item from ..enums import ComponentType +from ..components import UnfurledMediaItem if TYPE_CHECKING: from typing_extensions import Self @@ -47,9 +48,9 @@ class Thumbnail(Item[V]): Parameters ---------- - url: :class:`str` - The URL of the thumbnail. This can only point to a local attachment uploaded - within this item. URLs must match the ``attachment://file-name.extension`` + media: Union[:class:`str`, :class:`UnfurledMediaItem`] + The media of the thumbnail. This can be a string that points to a local + attachment uploaded within this item. URLs must match the ``attachment://file-name.extension`` structure. description: Optional[:class:`str`] The description of this thumbnail. Defaults to ``None``. @@ -57,11 +58,13 @@ class Thumbnail(Item[V]): Whether to flag this thumbnail as a spoiler. Defaults to ``False``. """ - def __init__(self, url: str, *, description: Optional[str] = None, spoiler: bool = False) -> None: - self.url: str = url + def __init__(self, media: Union[str, UnfurledMediaItem], *, description: Optional[str] = None, spoiler: bool = False) -> None: + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media self.description: Optional[str] = description self.spoiler: bool = spoiler + self._underlying = ThumbnailComponent._raw_construct() + @property def type(self) -> Literal[ComponentType.thumbnail]: return ComponentType.thumbnail @@ -73,14 +76,14 @@ def to_component_dict(self) -> Dict[str, Any]: return { 'type': self.type.value, 'spoiler': self.spoiler, - 'media': {'url': self.url}, + 'media': self.media.to_dict(), 'description': self.description, } @classmethod def from_component(cls, component: ThumbnailComponent) -> Self: return cls( - url=component.media.url, + media=component.media.url, description=component.description, spoiler=component.spoiler, ) diff --git a/discord/ui/view.py b/discord/ui/view.py index 4afcd9fad0ce..c2cef2248f56 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -255,6 +255,9 @@ def key(item: Item) -> int: # instead of grouping by row we will sort it so it is added # in order and should work as the original implementation + # this will append directly the v2 Components into the list + # and will add to an action row the loose items, such as + # buttons and selects for child in sorted(self._children, key=key): if child._is_v2(): components.append(child.to_component_dict()) From 0f04d48893da56eeb0cdc487c9cbdc522e39d63e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:48:31 +0100 Subject: [PATCH 215/354] chore: Remove views leftover --- discord/abc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index ae6a1fb15811..70531fb2005e 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1580,8 +1580,7 @@ async def send( You specified both ``file`` and ``files``, or you specified both ``embed`` and ``embeds``, or the ``reference`` object is not a :class:`~discord.Message`, - :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`, - or you specified both ``view`` and ``views``. + :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`. Returns --------- From 2b14f0b014f05806e67b840164ea259013d1bac5 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:51:36 +0100 Subject: [PATCH 216/354] chore: message attachments things --- discord/message.py | 302 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 298 insertions(+), 4 deletions(-) diff --git a/discord/message.py b/discord/message.py index 000747e787cf..c551138f7d3c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -27,6 +27,8 @@ import asyncio import datetime import re +import io +from os import PathLike from typing import ( Dict, TYPE_CHECKING, @@ -53,7 +55,7 @@ from .components import _component_factory from .embeds import Embed from .member import Member -from .flags import MessageFlags +from .flags import MessageFlags, AttachmentFlags from .file import File from .utils import escape_mentions, MISSING, deprecated from .http import handle_message_parameters @@ -63,7 +65,6 @@ from .threads import Thread from .channel import PartialMessageable from .poll import Poll -from .attachment import Attachment if TYPE_CHECKING: from typing_extensions import Self @@ -107,6 +108,7 @@ __all__ = ( + 'Attachment', 'Message', 'PartialMessage', 'MessageInteraction', @@ -138,6 +140,298 @@ def convert_emoji_reaction(emoji: Union[EmojiInputType, Reaction]) -> str: raise TypeError(f'emoji argument must be str, Emoji, or Reaction not {emoji.__class__.__name__}.') +class Attachment(Hashable): + """Represents an attachment from Discord. + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the attachment. + + .. describe:: x == y + + Checks if the attachment is equal to another attachment. + + .. describe:: x != y + + Checks if the attachment is not equal to another attachment. + + .. describe:: hash(x) + + Returns the hash of the attachment. + + .. versionchanged:: 1.7 + Attachment can now be casted to :class:`str` and is hashable. + + Attributes + ------------ + id: :class:`int` + The attachment ID. + size: :class:`int` + The attachment size in bytes. + height: Optional[:class:`int`] + The attachment's height, in pixels. Only applicable to images and videos. + width: Optional[:class:`int`] + The attachment's width, in pixels. Only applicable to images and videos. + filename: :class:`str` + The attachment's filename. + url: :class:`str` + The attachment URL. If the message this attachment was attached + to is deleted, then this will 404. + proxy_url: :class:`str` + The proxy URL. This is a cached version of the :attr:`~Attachment.url` in the + case of images. When the message is deleted, this URL might be valid for a few + minutes or not valid at all. + content_type: Optional[:class:`str`] + The attachment's `media type `_ + + .. versionadded:: 1.7 + description: Optional[:class:`str`] + The attachment's description. Only applicable to images. + + .. versionadded:: 2.0 + ephemeral: :class:`bool` + Whether the attachment is ephemeral. + + .. versionadded:: 2.0 + duration: Optional[:class:`float`] + The duration of the audio file in seconds. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + waveform: Optional[:class:`bytes`] + The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. + + .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 + """ + + __slots__ = ( + 'id', + 'size', + 'height', + 'width', + 'filename', + 'url', + 'proxy_url', + '_http', + 'content_type', + 'description', + 'ephemeral', + 'duration', + 'waveform', + '_flags', + 'title', + ) + + def __init__(self, *, data: AttachmentPayload, state: ConnectionState): + self.id: int = int(data['id']) + self.size: int = data['size'] + self.height: Optional[int] = data.get('height') + self.width: Optional[int] = data.get('width') + self.filename: str = data['filename'] + self.url: str = data['url'] + self.proxy_url: str = data['proxy_url'] + self._http = state.http + self.content_type: Optional[str] = data.get('content_type') + self.description: Optional[str] = data.get('description') + self.ephemeral: bool = data.get('ephemeral', False) + self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') + + waveform = data.get('waveform') + self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None + + self._flags: int = data.get('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + """:class:`AttachmentFlags`: The attachment's flags.""" + return AttachmentFlags._from_value(self._flags) + + def is_spoiler(self) -> bool: + """:class:`bool`: Whether this attachment contains a spoiler.""" + return self.filename.startswith('SPOILER_') + + def is_voice_message(self) -> bool: + """:class:`bool`: Whether this attachment is a voice message.""" + return self.duration is not None and 'voice-message' in self.url + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + return self.url or '' + + async def save( + self, + fp: Union[io.BufferedIOBase, PathLike[Any]], + *, + seek_begin: bool = True, + use_cached: bool = False, + ) -> int: + """|coro| + + Saves this attachment into a file-like object. + + Parameters + ----------- + fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`] + The file-like object to save this attachment to or the filename + to use. If a filename is passed then a file is created with that + filename and used instead. + seek_begin: :class:`bool` + Whether to seek to the beginning of the file after saving is + successfully done. + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + Raises + -------- + HTTPException + Saving the attachment failed. + NotFound + The attachment was deleted. + + Returns + -------- + :class:`int` + The number of bytes written. + """ + data = await self.read(use_cached=use_cached) + if isinstance(fp, io.BufferedIOBase): + written = fp.write(data) + if seek_begin: + fp.seek(0) + return written + else: + with open(fp, 'wb') as f: + return f.write(data) + + async def read(self, *, use_cached: bool = False) -> bytes: + """|coro| + + Retrieves the content of this attachment as a :class:`bytes` object. + + .. versionadded:: 1.1 + + Parameters + ----------- + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + Raises + ------ + HTTPException + Downloading the attachment failed. + Forbidden + You do not have permissions to access this attachment + NotFound + The attachment was deleted. + + Returns + ------- + :class:`bytes` + The contents of the attachment. + """ + url = self.proxy_url if use_cached else self.url + data = await self._http.get_from_cdn(url) + return data + + async def to_file( + self, + *, + filename: Optional[str] = MISSING, + description: Optional[str] = MISSING, + use_cached: bool = False, + spoiler: bool = False, + ) -> File: + """|coro| + + Converts the attachment into a :class:`File` suitable for sending via + :meth:`abc.Messageable.send`. + + .. versionadded:: 1.3 + + Parameters + ----------- + filename: Optional[:class:`str`] + The filename to use for the file. If not specified then the filename + of the attachment is used instead. + + .. versionadded:: 2.0 + description: Optional[:class:`str`] + The description to use for the file. If not specified then the + description of the attachment is used instead. + + .. versionadded:: 2.0 + use_cached: :class:`bool` + Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading + the attachment. This will allow attachments to be saved after deletion + more often, compared to the regular URL which is generally deleted right + after the message is deleted. Note that this can still fail to download + deleted attachments if too much time has passed and it does not work + on some types of attachments. + + .. versionadded:: 1.4 + spoiler: :class:`bool` + Whether the file is a spoiler. + + .. versionadded:: 1.4 + + Raises + ------ + HTTPException + Downloading the attachment failed. + Forbidden + You do not have permissions to access this attachment + NotFound + The attachment was deleted. + + Returns + ------- + :class:`File` + The attachment as a file suitable for sending. + """ + + data = await self.read(use_cached=use_cached) + file_filename = filename if filename is not MISSING else self.filename + file_description = description if description is not MISSING else self.description + return File(io.BytesIO(data), filename=file_filename, description=file_description, spoiler=spoiler) + + def to_dict(self) -> AttachmentPayload: + result: AttachmentPayload = { + 'filename': self.filename, + 'id': self.id, + 'proxy_url': self.proxy_url, + 'size': self.size, + 'url': self.url, + 'spoiler': self.is_spoiler(), + } + if self.height: + result['height'] = self.height + if self.width: + result['width'] = self.width + if self.content_type: + result['content_type'] = self.content_type + if self.description is not None: + result['description'] = self.description + return result + + class DeletedReferencedMessage: """A special sentinel type given when the resolved message reference points to a deleted message. @@ -238,9 +532,9 @@ def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): self.components: List[MessageComponentType] = [] for component_data in data.get('components', []): - component = _component_factory(component_data, state) + component = _component_factory(component_data, state) # type: ignore if component is not None: - self.components.append(component) + self.components.append(component) # type: ignore self._state: ConnectionState = state From f42b15fe135294a0e08375b6934ddc4a4dcdb3b1 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:52:16 +0100 Subject: [PATCH 217/354] chore: Remove attachment.py --- discord/types/attachment.py | 56 ------------------------------------- discord/types/components.py | 18 +++++++++--- discord/types/message.py | 18 +++++++++++- 3 files changed, 31 insertions(+), 61 deletions(-) delete mode 100644 discord/types/attachment.py diff --git a/discord/types/attachment.py b/discord/types/attachment.py deleted file mode 100644 index 0084c334c67e..000000000000 --- a/discord/types/attachment.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-present Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -from typing import Literal, Optional, TypedDict -from typing_extensions import NotRequired, Required - -from .snowflake import Snowflake - -LoadingState = Literal[0, 1, 2, 3] - -class AttachmentBase(TypedDict): - url: str - proxy_url: str - description: NotRequired[str] - spoiler: NotRequired[bool] - height: NotRequired[Optional[int]] - width: NotRequired[Optional[int]] - content_type: NotRequired[str] - flags: NotRequired[int] - - -class Attachment(AttachmentBase): - id: Snowflake - filename: str - size: int - ephemeral: NotRequired[bool] - duration_secs: NotRequired[float] - waveform: NotRequired[str] - title: NotRequired[str] - - -class UnfurledAttachment(AttachmentBase, total=False): - loading_state: Required[LoadingState] diff --git a/discord/types/components.py b/discord/types/components.py index a50cbdd1ec44..68aa6156df8d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -29,7 +29,6 @@ from .emoji import PartialEmoji from .channel import ChannelType -from .attachment import UnfurledAttachment ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] @@ -137,15 +136,26 @@ class TextComponent(ComponentBase): content: str +class UnfurledMediaItem(TypedDict): + url: str + proxy_url: str + height: NotRequired[Optional[int]] + width: NotRequired[Optional[int]] + content_type: NotRequired[str] + placeholder: str + loading_state: MediaItemLoadingState + flags: NotRequired[int] + + class ThumbnailComponent(ComponentBase): type: Literal[11] - media: UnfurledAttachment + media: UnfurledMediaItem description: NotRequired[Optional[str]] spoiler: NotRequired[bool] class MediaGalleryItem(TypedDict): - media: UnfurledAttachment + media: UnfurledMediaItem description: NotRequired[Optional[str]] spoiler: NotRequired[bool] @@ -157,7 +167,7 @@ class MediaGalleryComponent(ComponentBase): class FileComponent(ComponentBase): type: Literal[13] - file: UnfurledAttachment + file: UnfurledMediaItem spoiler: NotRequired[bool] diff --git a/discord/types/message.py b/discord/types/message.py index 81bfdd23baed..1d837d2d88e8 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -38,7 +38,6 @@ from .sticker import StickerItem from .threads import Thread from .poll import Poll -from .attachment import Attachment class PartialMessage(TypedDict): @@ -70,6 +69,23 @@ class Reaction(TypedDict): burst_colors: List[str] +class Attachment(TypedDict): + id: Snowflake + filename: str + size: int + url: str + proxy_url: str + height: NotRequired[Optional[int]] + width: NotRequired[Optional[int]] + description: NotRequired[str] + content_type: NotRequired[str] + spoiler: NotRequired[bool] + ephemeral: NotRequired[bool] + duration_secs: NotRequired[float] + waveform: NotRequired[str] + flags: NotRequired[int] + + MessageActivityType = Literal[1, 2, 3, 5] From 39998b4fb3435f71d5e44d7319eebeeac4cbdb01 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:53:37 +0100 Subject: [PATCH 218/354] chore: Revert double quotes --- discord/types/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/message.py b/discord/types/message.py index 1d837d2d88e8..6c260d44dbdf 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -229,7 +229,7 @@ class Message(PartialMessage): purchase_notification: NotRequired[PurchaseNotificationResponse] -AllowedMentionType = Literal["roles", "users", "everyone"] +AllowedMentionType = Literal['roles', 'users', 'everyone'] class AllowedMentions(TypedDict): From 28efb157eec0993a3e47e54bd33f6709237dd7d9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:42:37 +0100 Subject: [PATCH 219/354] chore: Finished first components v2 impl --- discord/components.py | 1 - discord/ui/file.py | 125 +++++++++++++++++++++++++ discord/ui/item.py | 5 +- discord/ui/media_gallery.py | 177 ++++++++++++++++++++++++++++++++++++ discord/ui/section.py | 8 ++ discord/ui/separator.py | 110 ++++++++++++++++++++++ discord/ui/text_display.py | 14 ++- discord/ui/thumbnail.py | 24 ++++- discord/ui/view.py | 7 +- 9 files changed, 462 insertions(+), 9 deletions(-) create mode 100644 discord/ui/file.py create mode 100644 discord/ui/media_gallery.py create mode 100644 discord/ui/separator.py diff --git a/discord/components.py b/discord/components.py index 7330b82e9448..f1364691016c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -986,7 +986,6 @@ def type(self) -> Literal[ComponentType.media_gallery]: def to_dict(self) -> MediaGalleryComponentPayload: return { - 'id': self.id, 'type': self.type.value, 'items': [item.to_dict() for item in self.items], } diff --git a/discord/ui/file.py b/discord/ui/file.py new file mode 100644 index 000000000000..b4285a654b5b --- /dev/null +++ b/discord/ui/file.py @@ -0,0 +1,125 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union + +from .item import Item +from ..components import FileComponent, UnfurledMediaItem +from ..enums import ComponentType + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('File',) + + +class File(Item[V]): + """Represents a UI file component. + + .. versionadded:: 2.6 + + Parameters + ---------- + media: Union[:class:`str`, :class:`UnfurledMediaItem`] + This file's media. If this is a string itmust point to a local + file uploaded within the parent view of this item, and must + meet the ``attachment://file-name.extension`` structure. + spoiler: :class:`bool` + Whether to flag this file as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this file component belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + spoiler: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = FileComponent._raw_construct( + media=UnfurledMediaItem(media) if isinstance(media, str) else media, + spoiler=spoiler, + ) + + self.row = row + + def _is_v2(self): + return True + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.file]: + return self._underlying.type + + @property + def media(self) -> UnfurledMediaItem: + """:class:`UnfurledMediaItem`: Returns this file media.""" + return self._underlying.media + + @media.setter + def media(self, value: UnfurledMediaItem) -> None: + self._underlying.media = value + + @property + def url(self) -> str: + """:class:`str`: Returns this file's url.""" + return self._underlying.media.url + + @url.setter + def url(self, value: str) -> None: + self._underlying.media = UnfurledMediaItem(value) + + @property + def spoiler(self) -> bool: + """:class:`bool`: Returns whether this file should be flagged as a spoiler.""" + return self._underlying.spoiler + + @spoiler.setter + def spoiler(self, value: bool) -> None: + self._underlying.spoiler = value + + def to_component_dict(self): + return self._underlying.to_dict() + + @classmethod + def from_component(cls, component: FileComponent) -> Self: + return cls( + media=component.media, + spoiler=component.spoiler, + ) diff --git a/discord/ui/item.py b/discord/ui/item.py index 2d2a3aaa6f88..aaf15cee648b 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -70,6 +70,7 @@ def __init__(self): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False + self._max_row: int = 5 if not self._is_v2() else 10 def to_component_dict(self) -> Dict[str, Any]: raise NotImplementedError @@ -109,10 +110,10 @@ def row(self) -> Optional[int]: def row(self, value: Optional[int]) -> None: if value is None: self._row = None - elif 5 > value >= 0: + elif self._max_row > value >= 0: self._row = value else: - raise ValueError('row cannot be negative or greater than or equal to 5') + raise ValueError('row cannot be negative or greater than or equal to 10') @property def width(self) -> int: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py new file mode 100644 index 000000000000..fa20af740651 --- /dev/null +++ b/discord/ui/media_gallery.py @@ -0,0 +1,177 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Literal, Optional, TypeVar + +from .item import Item +from ..enums import ComponentType +from ..components import ( + MediaGalleryItem, + MediaGalleryComponent, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('MediaGallery',) + + +class MediaGallery(Item[V]): + """Represents a UI media gallery. + + This can contain up to 10 :class:`MediaGalleryItem`s. + + .. versionadded:: 2.6 + + Parameters + ---------- + items: List[:class:`MediaGalleryItem`] + The initial items of this gallery. + row: Optional[:class:`int`] + The relative row this media gallery belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) -> None: + super().__init__() + + self._underlying = MediaGalleryComponent._raw_construct( + items=items, + ) + + self.row = row + + @property + def items(self) -> List[MediaGalleryItem]: + """List[:class:`MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" + return self._underlying.items.copy() + + @items.setter + def items(self, value: List[MediaGalleryItem]) -> None: + if len(value) > 10: + raise ValueError('media gallery only accepts up to 10 items') + + self._underlying.items = value + + def to_component_dict(self): + return self._underlying.to_dict() + + def _is_v2(self) -> bool: + return True + + def add_item(self, item: MediaGalleryItem) -> Self: + """Adds an item to this gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`MediaGalleryItem` + The item to add to the gallery. + + Raises + ------ + TypeError + A :class:`MediaGalleryItem` was not passed. + ValueError + Maximum number of items has been exceeded (10). + """ + + if len(self._underlying.items) >= 10: + raise ValueError('maximum number of items has been exceeded') + + if not isinstance(item, MediaGalleryItem): + raise TypeError(f'expected MediaGalleryItem not {item.__class__.__name__}') + + self._underlying.items.append(item) + return self + + def remove_item(self, item: MediaGalleryItem) -> Self: + """Removes an item from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`MediaGalleryItem` + The item to remove from the gallery. + """ + + try: + self._underlying.items.remove(item) + except ValueError: + pass + return self + + def insert_item_at(self, index: int, item: MediaGalleryItem) -> Self: + """Inserts an item before a specified index to the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + index: :class:`int` + The index of where to insert the item. + item: :class:`MediaGalleryItem` + The item to insert. + """ + + self._underlying.items.insert(index, item) + return self + + def clear_items(self) -> Self: + """Removes all items from the gallery. + + This function returns the class instance to allow for fluent-style + chaining. + """ + + self._underlying.items.clear() + return self + + @property + def type(self) -> Literal[ComponentType.media_gallery]: + return self._underlying.type + + @property + def width(self): + return 5 + + @classmethod + def from_component(cls, component: MediaGalleryComponent) -> Self: + return cls( + items=component.items, + ) diff --git a/discord/ui/section.py b/discord/ui/section.py index 0012d0118186..4da36f86f033 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -49,6 +49,13 @@ class Section(Item[V]): The text displays of this section. Up to 3. accessory: Optional[:class:`Item`] The section accessory. Defaults to ``None``. + row: Optional[:class:`int`] + The relative row this section belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ __slots__ = ( @@ -61,6 +68,7 @@ def __init__( children: List[Union[Item[Any], str]], *, accessory: Optional[Item[Any]] = None, + row: Optional[int] = None, ) -> None: super().__init__() if len(children) > 3: diff --git a/discord/ui/separator.py b/discord/ui/separator.py new file mode 100644 index 000000000000..c275fad82279 --- /dev/null +++ b/discord/ui/separator.py @@ -0,0 +1,110 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, Optional, TypeVar + +from .item import Item +from ..components import SeparatorComponent +from ..enums import SeparatorSize, ComponentType + +if TYPE_CHECKING: + from .view import View + +V = TypeVar('V', bound='View', covariant=True) + +__all__ = ('Separator',) + + +class Separator(Item[V]): + """Represents a UI separator. + + .. versionadded:: 2.6 + + Parameters + ---------- + visible: :class:`bool` + Whether this separator is visible. On the client side this + is whether a divider line should be shown or not. + spacing: :class:`SeparatorSize` + The spacing of this separator. + row: Optional[:class:`int`] + The relative row this separator belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) + """ + + def __init__( + self, + *, + visible: bool = True, + spacing: SeparatorSize = SeparatorSize.small, + row: Optional[int] = None, + ) -> None: + super().__init__() + self._underlying = SeparatorComponent._raw_construct( + spacing=spacing, + visible=visible, + ) + + self.row = row + + def _is_v2(self): + return True + + @property + def visible(self) -> bool: + """:class:`bool`: Whether this separator is visible. + + On the client side this is whether a divider line should + be shown or not. + """ + return self._underlying.visible + + @visible.setter + def visible(self, value: bool) -> None: + self._underlying.visible = value + + @property + def spacing(self) -> SeparatorSize: + """:class:`SeparatorSize`: The spacing of this separator.""" + return self._underlying.spacing + + @spacing.setter + def spacing(self, value: SeparatorSize) -> None: + self._underlying.spacing = value + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.separator]: + return self._underlying.type + + def to_component_dict(self): + return self._underlying.to_dict() diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 0daff9c89e5c..a51d604938b6 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypeVar +from typing import TYPE_CHECKING, Literal, Optional, TypeVar from .item import Item from ..components import TextDisplay as TextDisplayComponent @@ -46,16 +46,24 @@ class TextDisplay(Item[V]): ---------- content: :class:`str` The content of this text display. + row: Optional[:class:`int`] + The relative row this text display belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ - def __init__(self, content: str) -> None: + def __init__(self, content: str, *, row: Optional[int] = None) -> None: super().__init__() self.content: str = content - self._underlying = TextDisplayComponent._raw_construct( content=content, ) + self.row = row + def to_component_dict(self): return self._underlying.to_dict() diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 67e380f65c5c..fe1d962218c8 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -56,14 +56,34 @@ class Thumbnail(Item[V]): The description of this thumbnail. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this thumbnail as a spoiler. Defaults to ``False``. + row: Optional[:class:`int`] + The relative row this thumbnail belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ - def __init__(self, media: Union[str, UnfurledMediaItem], *, description: Optional[str] = None, spoiler: bool = False) -> None: + def __init__( + self, + media: Union[str, UnfurledMediaItem], + *, + description: Optional[str] = None, + spoiler: bool = False, + row: Optional[int] = None, + ) -> None: + super().__init__() + self.media: UnfurledMediaItem = UnfurledMediaItem(media) if isinstance(media, str) else media self.description: Optional[str] = description self.spoiler: bool = spoiler - self._underlying = ThumbnailComponent._raw_construct() + self.row = row + + @property + def width(self): + return 5 @property def type(self) -> Literal[ComponentType.thumbnail]: diff --git a/discord/ui/view.py b/discord/ui/view.py index c2cef2248f56..08714bf3a2af 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -685,7 +685,12 @@ async def schedule_dynamic_item_call( item._view = view item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore - +The relative row this text display belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) try: allow = await item.interaction_check(interaction) except Exception: From de8a7238f8cd282ba291b88875fd9177f95280ed Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:04:45 +0100 Subject: [PATCH 220/354] chore: docs and some changes --- discord/components.py | 2 + discord/ui/__init__.py | 6 ++ discord/ui/container.py | 4 +- discord/ui/file.py | 2 +- discord/ui/media_gallery.py | 2 +- discord/ui/section.py | 2 + discord/ui/separator.py | 11 ++- discord/ui/text_display.py | 8 ++ discord/ui/thumbnail.py | 2 +- discord/ui/view.py | 26 ++++- docs/api.rst | 29 ++++-- docs/interactions/api.rst | 188 +++++++++++++++++++++++++++++++++++- 12 files changed, 268 insertions(+), 14 deletions(-) diff --git a/discord/components.py b/discord/components.py index f1364691016c..cb1a50976379 100644 --- a/discord/components.py +++ b/discord/components.py @@ -91,6 +91,8 @@ 'SelectDefaultValue', 'SectionComponent', 'ThumbnailComponent', + 'UnfurledMediaItem', + 'MediaGalleryItem', 'MediaGalleryComponent', 'FileComponent', 'SectionComponent', diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 029717cb5294..62a78634c72d 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -17,3 +17,9 @@ from .text_input import * from .dynamic import * from .container import * +from .file import * +from .media_gallery import * +from .section import * +from .separator import * +from .text_display import * +from .thumbnail import * diff --git a/discord/ui/container.py b/discord/ui/container.py index a98b0d965ab5..978781dd8470 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -51,9 +51,9 @@ class Container(View, Item[V]): children: List[:class:`Item`] The initial children or :class:`View`s of this container. Can have up to 10 items. - accent_colour: Optional[:class:`~discord.Colour`] + accent_colour: Optional[:class:`.Colour`] The colour of the container. Defaults to ``None``. - accent_color: Optional[:class:`~discord.Color`] + accent_color: Optional[:class:`.Color`] The color of the container. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults diff --git a/discord/ui/file.py b/discord/ui/file.py index b4285a654b5b..fabf5b0f31d0 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -46,7 +46,7 @@ class File(Item[V]): Parameters ---------- - media: Union[:class:`str`, :class:`UnfurledMediaItem`] + media: Union[:class:`str`, :class:`.UnfurledMediaItem`] This file's media. If this is a string itmust point to a local file uploaded within the parent view of this item, and must meet the ``attachment://file-name.extension`` structure. diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index fa20af740651..93638d7f6690 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -51,7 +51,7 @@ class MediaGallery(Item[V]): Parameters ---------- - items: List[:class:`MediaGalleryItem`] + items: List[:class:`.MediaGalleryItem`] The initial items of this gallery. row: Optional[:class:`int`] The relative row this media gallery belongs to. By default diff --git a/discord/ui/section.py b/discord/ui/section.py index 4da36f86f033..fece9b053f13 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -78,6 +78,8 @@ def __init__( ] self.accessory: Optional[Item[Any]] = accessory + self.row = row + @property def type(self) -> Literal[ComponentType.section]: return ComponentType.section diff --git a/discord/ui/separator.py b/discord/ui/separator.py index c275fad82279..cc49adecb235 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -30,6 +30,8 @@ from ..enums import SeparatorSize, ComponentType if TYPE_CHECKING: + from typing_extensions import Self + from .view import View V = TypeVar('V', bound='View', covariant=True) @@ -47,7 +49,7 @@ class Separator(Item[V]): visible: :class:`bool` Whether this separator is visible. On the client side this is whether a divider line should be shown or not. - spacing: :class:`SeparatorSize` + spacing: :class:`discord.SeparatorSize` The spacing of this separator. row: Optional[:class:`int`] The relative row this separator belongs to. By default @@ -108,3 +110,10 @@ def type(self) -> Literal[ComponentType.separator]: def to_component_dict(self): return self._underlying.to_dict() + + @classmethod + def from_component(cls, component: SeparatorComponent) -> Self: + return cls( + visible=component.visible, + spacing=component.spacing, + ) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index a51d604938b6..9a70bd24728b 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -30,6 +30,8 @@ from ..enums import ComponentType if TYPE_CHECKING: + from typing_extensions import Self + from .view import View V = TypeVar('V', bound='View', covariant=True) @@ -77,3 +79,9 @@ def type(self) -> Literal[ComponentType.text_display]: def _is_v2(self) -> bool: return True + + @classmethod + def from_component(cls, component: TextDisplayComponent) -> Self: + return cls( + content=component.content, + ) diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index fe1d962218c8..ce178fb4cb5c 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -48,7 +48,7 @@ class Thumbnail(Item[V]): Parameters ---------- - media: Union[:class:`str`, :class:`UnfurledMediaItem`] + media: Union[:class:`str`, :class:`.UnfurledMediaItem`] The media of the thumbnail. This can be a string that points to a local attachment uploaded within this item. URLs must match the ``attachment://file-name.extension`` structure. diff --git a/discord/ui/view.py b/discord/ui/view.py index 08714bf3a2af..92ec768fa3f4 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -45,6 +45,7 @@ MediaGalleryComponent, FileComponent, SeparatorComponent, + ThumbnailComponent, ) # fmt: off @@ -86,7 +87,30 @@ def _component_to_item(component: Component) -> Item: from .select import BaseSelect return BaseSelect.from_component(component) - # TODO: convert V2 Components into Item's + if isinstance(component, SectionComponent): + from .section import Section + + return Section.from_component(component) + if isinstance(component, TextDisplayComponent): + from .text_display import TextDisplay + + return TextDisplay.from_component(component) + if isinstance(component, MediaGalleryComponent): + from .media_gallery import MediaGallery + + return MediaGallery.from_component(component) + if isinstance(component, FileComponent): + from .file import File + + return File.from_component(component) + if isinstance(component, SeparatorComponent): + from .separator import Separator + + return Separator.from_component(component) + if isinstance(component, ThumbnailComponent): + from .thumbnail import Thumbnail + + return Thumbnail.from_component(component) return Item.from_component(component) diff --git a/docs/api.rst b/docs/api.rst index 934335c5ac70..07e04ca77ae8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5427,8 +5427,6 @@ PollAnswer .. autoclass:: PollAnswer() :members: -.. _discord_api_data: - MessageSnapshot ~~~~~~~~~~~~~~~~~ @@ -5445,6 +5443,16 @@ ClientStatus .. autoclass:: ClientStatus() :members: +CallMessage +~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: CallMessage + +.. autoclass:: CallMessage() + :members: + +.. _discord_api_data: + Data Classes -------------- @@ -5748,12 +5756,21 @@ PollMedia .. autoclass:: PollMedia :members: -CallMessage -~~~~~~~~~~~~~~~~~~~ +UnfurledMediaItem +~~~~~~~~~~~~~~~~~ -.. attributetable:: CallMessage +.. attributetable:: UnfurledMediaItem -.. autoclass:: CallMessage() +.. autoclass:: UnfurledMediaItem + :members: + + +MediaGalleryItem +~~~~~~~~~~~~~~~~ + +.. attributetable:: MediaGalleryItem + +.. autoclass:: MediaGalleryItem :members: diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index feab669073ea..df2d7418dfff 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -113,6 +113,77 @@ TextInput :members: :inherited-members: + +SectionComponent +~~~~~~~~~~~~~~~~ + +.. attributetable:: SectionComponent + +.. autoclass:: SectionComponent() + :members: + :inherited-members: + + +ThumbnailComponent +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: ThumbnailComponent + +.. autoclass:: ThumbnailComponent() + :members: + :inherited-members: + + +TextDisplay +~~~~~~~~~~~ + +.. attributetable:: TextDisplay + +.. autoclass:: TextDisplay() + :members: + :inherited-members: + + +MediaGalleryComponent +~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: MediaGalleryComponent + +.. autoclass:: MediaGalleryComponent() + :members: + :inherited-members: + + +FileComponent +~~~~~~~~~~~~~ + +.. attributetable:: FileComponent + +.. autoclass:: FileComponent() + :members: + :inherited-members: + + +SeparatorComponent +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: SeparatorComponent + +.. autoclass:: SeparatorComponent() + :members: + :inherited-members: + + +Container +~~~~~~~~~ + +.. attributetable:: Container + +.. autoclass:: Container() + :members: + :inherited-members: + + AppCommand ~~~~~~~~~~~ @@ -299,7 +370,7 @@ Enumerations .. attribute:: action_row - Represents the group component which holds different components in a row. + Represents a component which holds different components in a row. .. attribute:: button @@ -329,6 +400,38 @@ Enumerations Represents a select in which both users and roles can be selected. + .. attribute:: channel_select + + Represents a channel select component. + + .. attribute:: section + + Represents a component which holds different components in a section. + + .. attribute:: text_display + + Represents a text display component. + + .. attribute:: thumbnail + + Represents a thumbnail component. + + .. attribute:: media_gallery + + Represents a media gallery component. + + .. attribute:: file + + Represents a file component. + + .. attribute:: separator + + Represents a separator component. + + .. attribute:: container + + Represents a component which holds different components in a container. + .. class:: ButtonStyle Represents the style of the button component. @@ -463,6 +566,19 @@ Enumerations The permission is for a user. +.. class:: SeparatorSize + + The separator's size type. + + .. versionadded:: 2.6 + + .. attribute:: small + + A small separator. + .. attribute:: large + + A large separator. + .. _discord_ui_kit: Bot UI Kit @@ -582,6 +698,76 @@ TextInput :members: :inherited-members: + +Container +~~~~~~~~~ + +.. attributetable:: discord.ui.Container + +.. autoclass:: discord.ui.Container + :members: + :inherited-members: + + +File +~~~~ + +.. attributetable:: discord.ui.File + +.. autoclass:: discord.ui.File + :members: + :inherited-members: + + +MediaGallery +~~~~~~~~~~~~ + +.. attributetable:: discord.ui.MediaGallery + +.. autoclass:: discord.ui.MediaGallery + :members: + :inherited-members: + + +Section +~~~~~~~ + +.. attributetable:: discord.ui.Section + +.. autoclass:: discord.ui.Section + :members: + :inherited-members: + + +Separator +~~~~~~~~~ + +.. attributetable:: discord.ui.Separator + +.. autoclass:: discord.ui.Separator + :members: + :inherited-members: + + +TextDisplay +~~~~~~~~~~~ + +.. attributetable:: discord.ui.TextDisplay + +.. autoclass:: discord.ui.TextDisplay + :members: + :inherited-members: + + +Thumbnail +~~~~~~~~~ + +.. attributetable:: discord.ui.Thumbnail + +.. autoclass:: discord.ui.Thumbnail + :members: + :inherited-members: + .. _discord_app_commands: Application Commands From 7824c3f544708cfc044470045539abb19cb56ddd Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:18:29 +0100 Subject: [PATCH 221/354] chore: fix everything lol --- discord/components.py | 43 ++++++++++++++++++------------------- discord/message.py | 3 +-- discord/types/components.py | 2 +- discord/ui/section.py | 9 ++++---- discord/ui/thumbnail.py | 5 ++--- discord/ui/view.py | 9 ++------ 6 files changed, 31 insertions(+), 40 deletions(-) diff --git a/discord/components.py b/discord/components.py index cb1a50976379..9769066386f0 100644 --- a/discord/components.py +++ b/discord/components.py @@ -79,6 +79,17 @@ ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput'] SectionComponentType = Union['TextDisplay', 'Button'] + MessageComponentType = Union[ + ActionRowChildComponentType, + SectionComponentType, + 'ActionRow', + 'SectionComponent', + 'ThumbnailComponent', + 'MediaGalleryComponent', + 'FileComponent', + 'SectionComponent', + 'Component', + ] __all__ = ( @@ -337,13 +348,9 @@ def __init__(self, data: SelectMenuPayload, /) -> None: self.placeholder: Optional[str] = data.get('placeholder') self.min_values: int = data.get('min_values', 1) self.max_values: int = data.get('max_values', 1) - self.options: List[SelectOption] = [ - SelectOption.from_dict(option) for option in data.get('options', []) - ] + self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])] self.disabled: bool = data.get('disabled', False) - self.channel_types: List[ChannelType] = [ - try_enum(ChannelType, t) for t in data.get('channel_types', []) - ] + self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])] self.default_values: List[SelectDefaultValue] = [ SelectDefaultValue.from_dict(d) for d in data.get('default_values', []) ] @@ -459,9 +466,7 @@ def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: elif isinstance(value, _EmojiTag): self._emoji = value._to_partial() else: - raise TypeError( - f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead' - ) + raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__.__name__} instead') else: self._emoji = None @@ -617,9 +622,7 @@ def type(self) -> SelectDefaultValueType: @type.setter def type(self, value: SelectDefaultValueType) -> None: if not isinstance(value, SelectDefaultValueType): - raise TypeError( - f'expected SelectDefaultValueType, received {value.__class__.__name__} instead' - ) + raise TypeError(f'expected SelectDefaultValueType, received {value.__class__.__name__} instead') self._type = value @@ -733,7 +736,7 @@ def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionStat self.components.append(component) # type: ignore # should be the correct type here try: - self.accessory: Optional[Component] = _component_factory(data['accessory']) + self.accessory: Optional[Component] = _component_factory(data['accessory']) # type: ignore except KeyError: self.accessory = None @@ -788,7 +791,7 @@ def type(self) -> Literal[ComponentType.thumbnail]: def to_dict(self) -> ThumbnailComponentPayload: return { - 'media': self.media.to_dict(), # type: ignroe + 'media': self.media.to_dict(), # pyright: ignore[reportReturnType] 'description': self.description, 'spoiler': self.spoiler, 'type': self.type.value, @@ -938,9 +941,7 @@ def __init__( self._state: Optional[ConnectionState] = None @classmethod - def _from_data( - cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState] - ) -> MediaGalleryItem: + def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem: media = data['media'] self = cls( media=media['url'], @@ -1089,7 +1090,7 @@ def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionSt self.spoiler: bool = data.get('spoiler', False) self._colour: Optional[Colour] try: - self._colour = Colour(data['accent_color']) + self._colour = Colour(data['accent_color']) # type: ignore except KeyError: self._colour = None @@ -1102,9 +1103,7 @@ def accent_colour(self) -> Optional[Colour]: """Optional[:class:`Color`]: The container's accent color.""" -def _component_factory( - data: ComponentPayload, state: Optional[ConnectionState] = None -) -> Optional[Component]: +def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]: if data['type'] == 1: return ActionRow(data) elif data['type'] == 2: @@ -1112,7 +1111,7 @@ def _component_factory( elif data['type'] == 4: return TextInput(data) elif data['type'] in (3, 5, 6, 7, 8): - return SelectMenu(data) + return SelectMenu(data) # type: ignore elif data['type'] == 9: return SectionComponent(data, state) elif data['type'] == 10: diff --git a/discord/message.py b/discord/message.py index c551138f7d3c..c0a853ce3cba 100644 --- a/discord/message.py +++ b/discord/message.py @@ -96,7 +96,7 @@ from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent from .abc import Snowflake from .abc import GuildChannel, MessageableChannel - from .components import ActionRow, ActionRowChildComponentType + from .components import MessageComponentType from .state import ConnectionState from .mentions import AllowedMentions from .user import User @@ -104,7 +104,6 @@ from .ui.view import View EmojiInputType = Union[Emoji, PartialEmoji, str] - MessageComponentType = Union[ActionRow, ActionRowChildComponentType] __all__ = ( diff --git a/discord/types/components.py b/discord/types/components.py index 68aa6156df8d..98201817ad3d 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -59,7 +59,7 @@ class ButtonComponent(ComponentBase): sku_id: NotRequired[str] -class SelectOption(ComponentBase): +class SelectOption(TypedDict): label: str value: str default: bool diff --git a/discord/ui/section.py b/discord/ui/section.py index fece9b053f13..f2b6554cad46 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -72,10 +72,8 @@ def __init__( ) -> None: super().__init__() if len(children) > 3: - raise ValueError('maximum number of children exceeded') - self._children: List[Item[Any]] = [ - c if isinstance(c, Item) else TextDisplay(c) for c in children - ] + raise ValueError('maximum number of children exceeded') + self._children: List[Item[Any]] = [c if isinstance(c, Item) else TextDisplay(c) for c in children] self.accessory: Optional[Item[Any]] = accessory self.row = row @@ -150,7 +148,8 @@ def clear_items(self) -> Self: @classmethod def from_component(cls, component: SectionComponent) -> Self: - from .view import _component_to_item # >circular import< + from .view import _component_to_item # >circular import< + return cls( children=[_component_to_item(c) for c in component.components], accessory=_component_to_item(component.accessory) if component.accessory else None, diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index ce178fb4cb5c..05e68b88166b 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -37,9 +37,8 @@ V = TypeVar('V', bound='View', covariant=True) -__all__ = ( - 'Thumbnail', -) +__all__ = ('Thumbnail',) + class Thumbnail(Item[V]): """Represents a UI Thumbnail. diff --git a/discord/ui/view.py b/discord/ui/view.py index 92ec768fa3f4..8dd7ca2d4628 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, Union +from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type from functools import partial from itertools import groupby @@ -709,12 +709,7 @@ async def schedule_dynamic_item_call( item._view = view item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore -The relative row this text display belongs to. By default - items are arranged automatically into those rows. If you'd - like to control the relative positioning of the row then - passing an index is advised. For example, row=1 will show - up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + try: allow = await item.interaction_check(interaction) except Exception: From 5d1300d9fc84bcbc7654b5c4435d7436f093aaa9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:26:01 +0100 Subject: [PATCH 222/354] fix: documentation errors --- discord/components.py | 2 ++ discord/ui/file.py | 2 +- discord/ui/media_gallery.py | 2 +- discord/ui/separator.py | 2 +- discord/ui/thumbnail.py | 2 +- discord/ui/view.py | 6 +++--- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/discord/components.py b/discord/components.py index 9769066386f0..c09adb9136c1 100644 --- a/discord/components.py +++ b/discord/components.py @@ -107,6 +107,8 @@ 'MediaGalleryComponent', 'FileComponent', 'SectionComponent', + 'Container', + 'TextDisplay', ) diff --git a/discord/ui/file.py b/discord/ui/file.py index fabf5b0f31d0..84ef4ef527ec 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -46,7 +46,7 @@ class File(Item[V]): Parameters ---------- - media: Union[:class:`str`, :class:`.UnfurledMediaItem`] + media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] This file's media. If this is a string itmust point to a local file uploaded within the parent view of this item, and must meet the ``attachment://file-name.extension`` structure. diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 93638d7f6690..b2da65df0ded 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -45,7 +45,7 @@ class MediaGallery(Item[V]): """Represents a UI media gallery. - This can contain up to 10 :class:`MediaGalleryItem`s. + This can contain up to 10 :class:`.MediaGalleryItem`s. .. versionadded:: 2.6 diff --git a/discord/ui/separator.py b/discord/ui/separator.py index cc49adecb235..2eadd2a4bc40 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -49,7 +49,7 @@ class Separator(Item[V]): visible: :class:`bool` Whether this separator is visible. On the client side this is whether a divider line should be shown or not. - spacing: :class:`discord.SeparatorSize` + spacing: :class:`.SeparatorSize` The spacing of this separator. row: Optional[:class:`int`] The relative row this separator belongs to. By default diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 05e68b88166b..cf9bfd3cc3d0 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -47,7 +47,7 @@ class Thumbnail(Item[V]): Parameters ---------- - media: Union[:class:`str`, :class:`.UnfurledMediaItem`] + media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] The media of the thumbnail. This can be a string that points to a local attachment uploaded within this item. URLs must match the ``attachment://file-name.extension`` structure. diff --git a/discord/ui/view.py b/discord/ui/view.py index 8dd7ca2d4628..e701d09e976d 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -61,7 +61,7 @@ from ..interactions import Interaction from ..message import Message - from ..types.components import Component as ComponentPayload + from ..types.components import ComponentBase as ComponentBasePayload from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload from ..state import ConnectionState from .modal import Modal @@ -802,11 +802,11 @@ def is_message_tracked(self, message_id: int) -> bool: def remove_message_tracking(self, message_id: int) -> Optional[View]: return self._synced_message_views.pop(message_id, None) - def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> None: + def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None: components: List[Component] = [] for component_data in data: - component = _component_factory(component_data, self._state) + component = _component_factory(component_data, self._state) # type: ignore if component is not None: components.append(component) From a1bc73b51b1b9570481fbde9df5ce4e70d4dfb2f Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:28:31 +0100 Subject: [PATCH 223/354] fix: add SeparatorComponent to __all__ --- discord/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/components.py b/discord/components.py index c09adb9136c1..b12de4f5efa2 100644 --- a/discord/components.py +++ b/discord/components.py @@ -109,6 +109,7 @@ 'SectionComponent', 'Container', 'TextDisplay', + 'SeparatorComponent', ) From 14d8f315362087a90d7af0ce0dceefa04dfa71fd Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:37:49 +0100 Subject: [PATCH 224/354] chore: Add missing enums to docs and fix docstrings --- discord/components.py | 7 ++----- discord/ui/container.py | 7 +++---- discord/ui/media_gallery.py | 10 +++++----- docs/api.rst | 21 +++++++++++++++++++++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/discord/components.py b/discord/components.py index b12de4f5efa2..d528f02ce409 100644 --- a/discord/components.py +++ b/discord/components.py @@ -769,7 +769,7 @@ class ThumbnailComponent(Component): Attributes ---------- - media: :class:`UnfurledAttachment` + media: :class:`UnfurledMediaItem` The media for this thumbnail. description: Optional[:class:`str`] The description shown within this thumbnail. @@ -832,9 +832,6 @@ class UnfurledMediaItem(AssetMixin): """Represents an unfurled media item that can be used on :class:`MediaGalleryItem`s. - Unlike :class:`UnfurledAttachment` this represents a media item - not yet stored on Discord and thus it does not have any data. - Parameters ---------- url: :class:`str` @@ -1004,7 +1001,7 @@ class FileComponent(Component): Attributes ---------- - media: :class:`UnfurledAttachment` + media: :class:`UnfurledMediaItem` The unfurled attachment contents of the file. spoiler: :class:`bool` Whether this file is flagged as a spoiler. diff --git a/discord/ui/container.py b/discord/ui/container.py index 978781dd8470..170d6eeca92d 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -42,7 +42,7 @@ class Container(View, Item[V]): - """Represents a Components V2 Container. + """Represents a UI container. .. versionadded:: 2.6 @@ -53,7 +53,7 @@ class Container(View, Item[V]): items. accent_colour: Optional[:class:`.Colour`] The colour of the container. Defaults to ``None``. - accent_color: Optional[:class:`.Color`] + accent_color: Optional[:class:`.Colour`] The color of the container. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults @@ -103,7 +103,7 @@ def children(self, value: List[Item[Any]]) -> None: @property def accent_colour(self) -> Optional[Colour]: - """Optional[:class:`~discord.Colour`]: The colour of the container, or ``None``.""" + """Optional[:class:`discord.Colour`]: The colour of the container, or ``None``.""" return self._colour @accent_colour.setter @@ -111,7 +111,6 @@ def accent_colour(self, value: Optional[Colour]) -> None: self._colour = value accent_color = accent_colour - """Optional[:class:`~discord.Color`]: The color of the container, or ``None``.""" @property def type(self) -> Literal[ComponentType.container]: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index b2da65df0ded..88991d40bd46 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -73,7 +73,7 @@ def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) @property def items(self) -> List[MediaGalleryItem]: - """List[:class:`MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" + """List[:class:`.MediaGalleryItem`]: Returns a read-only list of this gallery's items.""" return self._underlying.items.copy() @items.setter @@ -97,13 +97,13 @@ def add_item(self, item: MediaGalleryItem) -> Self: Parameters ---------- - item: :class:`MediaGalleryItem` + item: :class:`.MediaGalleryItem` The item to add to the gallery. Raises ------ TypeError - A :class:`MediaGalleryItem` was not passed. + A :class:`.MediaGalleryItem` was not passed. ValueError Maximum number of items has been exceeded (10). """ @@ -125,7 +125,7 @@ def remove_item(self, item: MediaGalleryItem) -> Self: Parameters ---------- - item: :class:`MediaGalleryItem` + item: :class:`.MediaGalleryItem` The item to remove from the gallery. """ @@ -145,7 +145,7 @@ def insert_item_at(self, index: int, item: MediaGalleryItem) -> Self: ---------- index: :class:`int` The index of where to insert the item. - item: :class:`MediaGalleryItem` + item: :class:`.MediaGalleryItem` The item to insert. """ diff --git a/docs/api.rst b/docs/api.rst index 07e04ca77ae8..d331715c30c0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3863,6 +3863,27 @@ of :class:`enum.Enum`. An alias for :attr:`.reply`. + +.. class:: MediaItemLoadingState + + Represents a :class:`UnfurledMediaItem` load state. + + .. attribute:: unknown + + Unknown load state. + + .. attribute:: loading + + The media item is still loading. + + .. attribute:: loaded + + The media item is loaded. + + .. attribute:: not_found + + The media item was not found. + .. _discord-api-audit-logs: Audit Log Data From 4202ef4c7ea08803b66b7cda25f9ef2031a4f24c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:39:49 +0100 Subject: [PATCH 225/354] chore: Format ValueError no row.setter to show the maxrow and not 10 --- discord/ui/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/item.py b/discord/ui/item.py index aaf15cee648b..bbd90464a603 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -113,7 +113,7 @@ def row(self, value: Optional[int]) -> None: elif self._max_row > value >= 0: self._row = value else: - raise ValueError('row cannot be negative or greater than or equal to 10') + raise ValueError(f'row cannot be negative or greater than or equal to {self._max_row}') @property def width(self) -> int: From 15ec28b8701bb27355745106b1a86c30b8f4d9dd Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:42:43 +0100 Subject: [PATCH 226/354] chore: yet more docs fix --- discord/components.py | 5 +++-- discord/ui/file.py | 4 ++-- discord/ui/separator.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/discord/components.py b/discord/components.py index d528f02ce409..ed15e1c48167 100644 --- a/discord/components.py +++ b/discord/components.py @@ -839,8 +839,10 @@ class UnfurledMediaItem(AssetMixin): Attributes ---------- + url: :class:`str` + The URL of this media item. proxy_url: Optional[:class:`str`] - The proxy URL. This is a cached version of the :attr:`~UnfurledMediaItem.url` in the + The proxy URL. This is a cached version of the :attr:`.url` in the case of images. When the message is deleted, this URL might be valid for a few minutes or not valid at all. height: Optional[:class:`int`] @@ -1100,7 +1102,6 @@ def accent_colour(self) -> Optional[Colour]: return self._colour accent_color = accent_colour - """Optional[:class:`Color`]: The container's accent color.""" def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]: diff --git a/discord/ui/file.py b/discord/ui/file.py index 84ef4ef527ec..3ff6c7d0f04f 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -46,7 +46,7 @@ class File(Item[V]): Parameters ---------- - media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] + media: Union[:class:`str`, :class:`.UnfurledMediaItem`] This file's media. If this is a string itmust point to a local file uploaded within the parent view of this item, and must meet the ``attachment://file-name.extension`` structure. @@ -89,7 +89,7 @@ def type(self) -> Literal[ComponentType.file]: @property def media(self) -> UnfurledMediaItem: - """:class:`UnfurledMediaItem`: Returns this file media.""" + """:class:`.UnfurledMediaItem`: Returns this file media.""" return self._underlying.media @media.setter diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 2eadd2a4bc40..33401f880b95 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -93,7 +93,7 @@ def visible(self, value: bool) -> None: @property def spacing(self) -> SeparatorSize: - """:class:`SeparatorSize`: The spacing of this separator.""" + """:class:`.SeparatorSize`: The spacing of this separator.""" return self._underlying.spacing @spacing.setter From 86d967cbcd364279291e9a12e22d8adeaed9f7b5 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:47:15 +0100 Subject: [PATCH 227/354] fix: Docs failing due to :class: ames --- discord/components.py | 3 +-- discord/ui/container.py | 2 +- discord/ui/media_gallery.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/discord/components.py b/discord/components.py index ed15e1c48167..962be86f7038 100644 --- a/discord/components.py +++ b/discord/components.py @@ -829,8 +829,7 @@ def to_dict(self) -> TextComponentPayload: class UnfurledMediaItem(AssetMixin): - """Represents an unfurled media item that can be used on - :class:`MediaGalleryItem`s. + """Represents an unfurled media item. Parameters ---------- diff --git a/discord/ui/container.py b/discord/ui/container.py index 170d6eeca92d..b49e1a7007fc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -49,7 +49,7 @@ class Container(View, Item[V]): Parameters ---------- children: List[:class:`Item`] - The initial children or :class:`View`s of this container. Can have up to 10 + The initial children or :class:`View` s of this container. Can have up to 10 items. accent_colour: Optional[:class:`.Colour`] The colour of the container. Defaults to ``None``. diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 88991d40bd46..4bc6c826f630 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -45,7 +45,7 @@ class MediaGallery(Item[V]): """Represents a UI media gallery. - This can contain up to 10 :class:`.MediaGalleryItem`s. + This can contain up to 10 :class:`.MediaGalleryItem` s. .. versionadded:: 2.6 From e7693d91346ecae3effbe9bf6cfdb8f93e884aa7 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:59:40 +0100 Subject: [PATCH 228/354] chore: buttons cannot be outside action rows --- discord/ui/button.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index b4df36aed8a9..43bd3a8b0f9d 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -73,11 +73,10 @@ class Button(Item[V]): The emoji of the button, if available. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 - rows in a :class:`View`, but up to 10 on a :class:`Container`. By default, - items are arranged automatically into those rows. If you'd like to control the - relative positioning of the row then passing an index is advised. For example, - row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed). + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). sku_id: Optional[:class:`int`] The SKU ID this button sends you to. Can't be combined with ``url``, ``label``, ``emoji`` nor ``custom_id``. @@ -305,11 +304,10 @@ def button( or a full :class:`.Emoji`. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 - rows in a :class:`View`, but up to 10 on a :class:`Container`. By default, - items are arranged automatically into those rows. If you'd like to control the - relative positioning of the row then passing an index is advised. For example, - row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 4 or 9 (i.e. zero indexed). + rows. By default, items are arranged automatically into those 5 rows. If you'd + like to control the relative positioning of the row then passing an index is advised. + For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 4 (i.e. zero indexed). """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: From 5fda19eb917f4ff346b5ffad8eb209e6f8e7b46b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:05:41 +0100 Subject: [PATCH 229/354] chore: add ui.Section.is_dispatchable --- discord/ui/section.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index f2b6554cad46..f8b8ea4e294f 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -89,6 +89,15 @@ def width(self): def _is_v2(self) -> bool: return True + # Accessory can be a button, and thus it can have a callback so, maybe + # allow for section to be dispatchable and make the callback func + # be accessory component callback, only called if accessory is + # dispatchable? + def is_dispatchable(self) -> bool: + if self.accessory: + return self.accessory.is_dispatchable() + return False + def add_item(self, item: Union[str, Item[Any]]) -> Self: """Adds an item to this section. From 0a0396889c9fc9014c9a5dc1d05d4e23383dfa3d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:41:10 +0100 Subject: [PATCH 230/354] chore: fix errors TextDisplay attribute error when doing TextDisplay.to_component_dict() View.to_components() appending Item objects instead of Item.to_component_dict() Changed View.to_components() sorting key --- discord/components.py | 9 ++------- discord/types/components.py | 2 +- discord/ui/container.py | 22 ++++++++++++--------- discord/ui/section.py | 38 +++++++++++++++++++++++-------------- discord/ui/text_display.py | 10 +++++----- discord/ui/view.py | 38 ++++++++++++++++++++++--------------- 6 files changed, 68 insertions(+), 51 deletions(-) diff --git a/discord/components.py b/discord/components.py index 962be86f7038..f06eda2f6948 100644 --- a/discord/components.py +++ b/discord/components.py @@ -732,17 +732,13 @@ class SectionComponent(Component): def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: self.components: List[SectionComponentType] = [] + self.accessory: Component = _component_factory(data['accessory'], state) for component_data in data['components']: component = _component_factory(component_data, state) if component is not None: self.components.append(component) # type: ignore # should be the correct type here - try: - self.accessory: Optional[Component] = _component_factory(data['accessory']) # type: ignore - except KeyError: - self.accessory = None - @property def type(self) -> Literal[ComponentType.section]: return ComponentType.section @@ -751,9 +747,8 @@ def to_dict(self) -> SectionComponentPayload: payload: SectionComponentPayload = { 'type': self.type.value, 'components': [c.to_dict() for c in self.components], + 'accessory': self.accessory.to_dict() } - if self.accessory: - payload['accessory'] = self.accessory.to_dict() return payload diff --git a/discord/types/components.py b/discord/types/components.py index 98201817ad3d..bb241c9ac6ff 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -128,7 +128,7 @@ class SelectMenu(SelectComponent): class SectionComponent(ComponentBase): type: Literal[9] components: List[Union[TextComponent, ButtonComponent]] - accessory: NotRequired[ComponentBase] + accessory: ComponentBase class TextComponent(ComponentBase): diff --git a/discord/ui/container.py b/discord/ui/container.py index b49e1a7007fc..2acf95d20bad 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -29,6 +29,7 @@ from .view import View, _component_to_item from .dynamic import DynamicItem from ..enums import ComponentType +from ..utils import MISSING if TYPE_CHECKING: from typing_extensions import Self @@ -61,13 +62,20 @@ class Container(View, Item[V]): timeout: Optional[:class:`float`] Timeout in seconds from last interaction with the UI before no longer accepting input. If ``None`` then there is no timeout. + row: Optional[:class:`int`] + The relative row this container belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 9 (i.e. zero indexed) """ __discord_ui_container__ = True def __init__( self, - children: List[Item[Any]], + children: List[Item[Any]] = MISSING, *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, @@ -76,9 +84,10 @@ def __init__( row: Optional[int] = None, ) -> None: super().__init__(timeout=timeout) - if len(children) + len(self._children) > 10: - raise ValueError('maximum number of components exceeded') - self._children.extend(children) + if children is not MISSING: + if len(children) + len(self._children) > 10: + raise ValueError('maximum number of components exceeded') + self._children.extend(children) self.spoiler: bool = spoiler self._colour = accent_colour or accent_color @@ -87,11 +96,6 @@ def __init__( self._rendered_row: Optional[int] = None self.row: Optional[int] = row - def _init_children(self) -> List[Item[Self]]: - if self.__weights.max_weight != 10: - self.__weights.max_weight = 10 - return super()._init_children() - @property def children(self) -> List[Item[Self]]: """List[:class:`Item`]: The children of this container.""" diff --git a/discord/ui/section.py b/discord/ui/section.py index f8b8ea4e294f..ce87b99f4bf4 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -28,6 +28,7 @@ from .item import Item from .text_display import TextDisplay from ..enums import ComponentType +from ..utils import MISSING if TYPE_CHECKING: from typing_extensions import Self @@ -37,6 +38,8 @@ V = TypeVar('V', bound='View', covariant=True) +__all__ = ('Section',) + class Section(Item[V]): """Represents a UI section. @@ -47,8 +50,8 @@ class Section(Item[V]): ---------- children: List[Union[:class:`str`, :class:`TextDisplay`]] The text displays of this section. Up to 3. - accessory: Optional[:class:`Item`] - The section accessory. Defaults to ``None``. + accessory: :class:`Item` + The section accessory. row: Optional[:class:`int`] The relative row this section belongs to. By default items are arranged automatically into those rows. If you'd @@ -65,16 +68,23 @@ class Section(Item[V]): def __init__( self, - children: List[Union[Item[Any], str]], + children: List[Union[Item[Any], str]] = MISSING, *, - accessory: Optional[Item[Any]] = None, + accessory: Item[Any], row: Optional[int] = None, ) -> None: super().__init__() - if len(children) > 3: - raise ValueError('maximum number of children exceeded') - self._children: List[Item[Any]] = [c if isinstance(c, Item) else TextDisplay(c) for c in children] - self.accessory: Optional[Item[Any]] = accessory + self._children: List[Item[Any]] = [] + if children is not MISSING: + if len(children) > 3: + raise ValueError('maximum number of children exceeded') + self._children.extend( + [ + c if isinstance(c, Item) + else TextDisplay(c) for c in children + ], + ) + self.accessory: Item[Any] = accessory self.row = row @@ -106,13 +116,14 @@ def add_item(self, item: Union[str, Item[Any]]) -> Self: Parameters ---------- - item: Union[:class:`str`, :class:`TextDisplay`] - The text display to add. + item: Union[:class:`str`, :class:`Item`] + The items to append, if it is a string it automatically wrapped around + :class:`TextDisplay`. Raises ------ TypeError - A :class:`TextDisplay` was not passed. + An :class:`Item` or :class:`str` was not passed. ValueError Maximum number of children has been exceeded (3). """ @@ -161,14 +172,13 @@ def from_component(cls, component: SectionComponent) -> Self: return cls( children=[_component_to_item(c) for c in component.components], - accessory=_component_to_item(component.accessory) if component.accessory else None, + accessory=_component_to_item(component.accessory), ) def to_component_dict(self) -> Dict[str, Any]: data = { 'components': [c.to_component_dict() for c in self._children], 'type': self.type.value, + 'accessory': self.accessory.to_component_dict() } - if self.accessory: - data['accessory'] = self.accessory.to_component_dict() return data diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 9a70bd24728b..1bf88678d798 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -60,14 +60,14 @@ class TextDisplay(Item[V]): def __init__(self, content: str, *, row: Optional[int] = None) -> None: super().__init__() self.content: str = content - self._underlying = TextDisplayComponent._raw_construct( - content=content, - ) self.row = row def to_component_dict(self): - return self._underlying.to_dict() + return { + 'type': self.type.value, + 'content': self.content, + } @property def width(self): @@ -75,7 +75,7 @@ def width(self): @property def type(self) -> Literal[ComponentType.text_display]: - return self._underlying.type + return ComponentType.text_display def _is_v2(self) -> bool: return True diff --git a/discord/ui/view.py b/discord/ui/view.py index e701d09e976d..6ac69d66e1b8 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type +from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, Union from functools import partial from itertools import groupby @@ -119,13 +119,11 @@ class _ViewWeights: # fmt: off __slots__ = ( 'weights', - 'max_weight', ) # fmt: on def __init__(self, children: List[Item]): self.weights: List[int] = [0, 0, 0, 0, 0] - self.max_weight: int = 5 key = lambda i: sys.maxsize if i.row is None else i.row children = sorted(children, key=key) @@ -146,8 +144,8 @@ def add_item(self, item: Item) -> None: self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width - if total > self.max_weight: - raise ValueError(f'item would not fit at row {item.row} ({total} > {self.max_weight} width)') + if total > 5: + raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)') self.weights[item.row] = total item._rendered_row = item.row else: @@ -196,15 +194,15 @@ class View: __discord_ui_view__: ClassVar[bool] = True __discord_ui_modal__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False - __view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] + __view_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] def __init_subclass__(cls) -> None: super().__init_subclass__() - children: Dict[str, ItemCallbackType[Any, Any]] = {} + children: Dict[str, Union[ItemCallbackType[Any, Any], Item]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): - if hasattr(member, '__discord_ui_model_type__'): + if hasattr(member, '__discord_ui_model_type__') or isinstance(member, Item): children[name] = member if len(children) > 25: @@ -214,12 +212,16 @@ def __init_subclass__(cls) -> None: def _init_children(self) -> List[Item[Self]]: children = [] + for func in self.__view_children_items__: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) # type: ignore - item._view = self - setattr(self, func.__name__, item) - children.append(item) + if isinstance(func, Item): + children.append(func) + else: + item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item.callback = _ViewCallback(func, self, item) # type: ignore + item._view = self + setattr(self, func.__name__, item) + children.append(item) return children def __init__(self, *, timeout: Optional[float] = 180.0): @@ -275,7 +277,13 @@ def to_components(self) -> List[Dict[str, Any]]: # v2 components def key(item: Item) -> int: - return item._rendered_row or 0 + if item._rendered_row is not None: + return item._rendered_row + + try: + return self._children.index(item) + except ValueError: + return 0 # instead of grouping by row we will sort it so it is added # in order and should work as the original implementation @@ -290,7 +298,7 @@ def key(item: Item) -> int: index = rows_index.get(row) if index is not None: - components[index]['components'].append(child) + components[index]['components'].append(child.to_component_dict()) else: components.append( { From a4389cbe7e60cee8473763abd0d1da4d27d32c0d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:42:54 +0100 Subject: [PATCH 231/354] chore: Revert change View.to_components() sorting key --- discord/ui/view.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 6ac69d66e1b8..208299c3a2b4 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -277,13 +277,7 @@ def to_components(self) -> List[Dict[str, Any]]: # v2 components def key(item: Item) -> int: - if item._rendered_row is not None: - return item._rendered_row - - try: - return self._children.index(item) - except ValueError: - return 0 + return item._rendered_row or 0 # instead of grouping by row we will sort it so it is added # in order and should work as the original implementation From 70bdcfa0b741b55e7566f6c21ebeadd980f9b202 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:03:03 +0100 Subject: [PATCH 232/354] chore: Update item _view attr and # type: ignore self.accessory in section --- discord/components.py | 2 +- discord/ui/view.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index f06eda2f6948..af3e23e7b873 100644 --- a/discord/components.py +++ b/discord/components.py @@ -732,7 +732,7 @@ class SectionComponent(Component): def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: self.components: List[SectionComponentType] = [] - self.accessory: Component = _component_factory(data['accessory'], state) + self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore for component_data in data['components']: component = _component_factory(component_data, state) diff --git a/discord/ui/view.py b/discord/ui/view.py index 208299c3a2b4..3769d4c4c92f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -215,6 +215,7 @@ def _init_children(self) -> List[Item[Self]]: for func in self.__view_children_items__: if isinstance(func, Item): + func._view = self children.append(func) else: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) From 568a3c396fec5359f51dc9894155d757d012df7b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:04:12 +0100 Subject: [PATCH 233/354] chore: Run black --- discord/components.py | 2 +- discord/ui/section.py | 7 ++----- discord/ui/view.py | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/discord/components.py b/discord/components.py index af3e23e7b873..fc9a77de80ae 100644 --- a/discord/components.py +++ b/discord/components.py @@ -747,7 +747,7 @@ def to_dict(self) -> SectionComponentPayload: payload: SectionComponentPayload = { 'type': self.type.value, 'components': [c.to_dict() for c in self.components], - 'accessory': self.accessory.to_dict() + 'accessory': self.accessory.to_dict(), } return payload diff --git a/discord/ui/section.py b/discord/ui/section.py index ce87b99f4bf4..c0dfbfae74de 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -79,10 +79,7 @@ def __init__( if len(children) > 3: raise ValueError('maximum number of children exceeded') self._children.extend( - [ - c if isinstance(c, Item) - else TextDisplay(c) for c in children - ], + [c if isinstance(c, Item) else TextDisplay(c) for c in children], ) self.accessory: Item[Any] = accessory @@ -179,6 +176,6 @@ def to_component_dict(self) -> Dict[str, Any]: data = { 'components': [c.to_component_dict() for c in self._children], 'type': self.type.value, - 'accessory': self.accessory.to_component_dict() + 'accessory': self.accessory.to_component_dict(), } return data diff --git a/discord/ui/view.py b/discord/ui/view.py index 3769d4c4c92f..92910e7a6b3a 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,7 +23,21 @@ """ from __future__ import annotations -from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type, Union +from typing import ( + Any, + Callable, + ClassVar, + Coroutine, + Dict, + Iterator, + List, + Optional, + Sequence, + TYPE_CHECKING, + Tuple, + Type, + Union, +) from functools import partial from itertools import groupby From bfae3a5183b390ea05962d6144015a385bfe0079 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:13:08 +0100 Subject: [PATCH 234/354] chore: Make type the first key on to_components_dict --- discord/components.py | 2 +- discord/ui/section.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index fc9a77de80ae..4321d79dc7e0 100644 --- a/discord/components.py +++ b/discord/components.py @@ -1018,9 +1018,9 @@ def type(self) -> Literal[ComponentType.file]: def to_dict(self) -> FileComponentPayload: return { + 'type': self.type.value, 'file': self.media.to_dict(), # type: ignore 'spoiler': self.spoiler, - 'type': self.type.value, } diff --git a/discord/ui/section.py b/discord/ui/section.py index c0dfbfae74de..5a0ec7f27398 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -174,8 +174,8 @@ def from_component(cls, component: SectionComponent) -> Self: def to_component_dict(self) -> Dict[str, Any]: data = { - 'components': [c.to_component_dict() for c in self._children], 'type': self.type.value, + 'components': [c.to_component_dict() for c in self._children], 'accessory': self.accessory.to_component_dict(), } return data From 869b68f68b899d9f2a1a7b08f01fce2df13cbfa9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:44:45 +0100 Subject: [PATCH 235/354] fix: _ViewWeights.v2_weights always returning False --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 92910e7a6b3a..e490f1444cee 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -176,7 +176,7 @@ def clear(self) -> None: self.weights = [0, 0, 0, 0, 0] def v2_weights(self) -> bool: - return sum(1 if w > 0 else 0 for w in self.weights) > 5 + return len(self.weights) > 5 class _ViewCallback: From faa31ffc5270bca4c4d394c8bcc7c7e8b8fc8e9b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:57:03 +0100 Subject: [PATCH 236/354] chore: Add notes and versionaddeds --- discord/components.py | 67 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/discord/components.py b/discord/components.py index 4321d79dc7e0..ac4ff987e51e 100644 --- a/discord/components.py +++ b/discord/components.py @@ -719,7 +719,7 @@ class SectionComponent(Component): ---------- components: List[Union[:class:`TextDisplay`, :class:`Button`]] The components on this section. - accessory: Optional[:class:`Component`] + accessory: :class:`Component` The section accessory. """ @@ -762,6 +762,8 @@ class ThumbnailComponent(Component): The user constructible and usable type to create a thumbnail is :class:`discord.ui.Thumbnail` not this one. + .. versionadded:: 2.6 + Attributes ---------- media: :class:`UnfurledMediaItem` @@ -772,7 +774,13 @@ class ThumbnailComponent(Component): Whether this thumbnail is flagged as a spoiler. """ - __slots__ = () + __slots__ = ( + 'media', + 'spoiler', + 'description', + ) + + __repr_info__ = __slots__ def __init__( self, @@ -801,6 +809,11 @@ class TextDisplay(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type to create a text display is + :class:`discord.ui.TextDisplay` not this one. + .. versionadded:: 2.6 Attributes @@ -809,6 +822,10 @@ class TextDisplay(Component): The content that this display shows. """ + __slots__ = ('content',) + + __repr_info__ = __slots__ + def __init__(self, data: TextComponentPayload) -> None: self.content: str = data['content'] @@ -826,6 +843,8 @@ def to_dict(self) -> TextComponentPayload: class UnfurledMediaItem(AssetMixin): """Represents an unfurled media item. + .. versionadded:: 2.6 + Parameters ---------- url: :class:`str` @@ -896,6 +915,9 @@ def _update(self, data: UnfurledMediaItemPayload, state: Optional[ConnectionStat self.loading_state = try_enum(MediaItemLoadingState, data['loading_state']) self._state = state + def __repr__(self) -> str: + return f'' + def to_dict(self): return { 'url': self.url, @@ -905,6 +927,8 @@ def to_dict(self): class MediaGalleryItem: """Represents a :class:`MediaGalleryComponent` media item. + .. versionadded:: 2.6 + Parameters ---------- media: Union[:class:`str`, :class:`UnfurledMediaItem`] @@ -936,6 +960,9 @@ def __init__( self.spoiler: bool = spoiler self._state: Optional[ConnectionState] = None + def __repr__(self) -> str: + return f'' + @classmethod def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem: media = data['media'] @@ -968,13 +995,22 @@ class MediaGalleryComponent(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type for creating a media gallery is + :class:`discord.ui.MediaGallery` not this one. + + .. versionadded:: 2.6 + Attributes ---------- items: List[:class:`MediaGalleryItem`] The items this gallery has. """ - __slots__ = ('items', 'id') + __slots__ = ('items',) + + __repr_info__ = __slots__ def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None: self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state) @@ -995,6 +1031,13 @@ class FileComponent(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type for create a file component is + :class:`discord.ui.File` not this one. + + .. versionadded:: 2.6 + Attributes ---------- media: :class:`UnfurledMediaItem` @@ -1008,6 +1051,8 @@ class FileComponent(Component): 'spoiler', ) + __repr_info__ = __slots__ + def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state) self.spoiler: bool = data.get('spoiler', False) @@ -1029,6 +1074,13 @@ class SeparatorComponent(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type for creating a separator is + :class:`discord.ui.Separator` not this one. + + .. versionadded:: 2.6 + Attributes ---------- spacing: :class:`SeparatorSize` @@ -1042,6 +1094,8 @@ class SeparatorComponent(Component): 'visible', ) + __repr_info__ = __slots__ + def __init__( self, data: SeparatorComponentPayload, @@ -1066,6 +1120,13 @@ class Container(Component): This inherits from :class:`Component`. + .. note:: + + The user constructible and usable type for creating a container is + :class:`discord.ui.Container` not this one. + + .. versionadded:: 2.6 + Attributes ---------- children: :class:`Component` From de5720e6593922dcbe2b03b6046dee2e18531b02 Mon Sep 17 00:00:00 2001 From: dolfies Date: Sun, 2 Mar 2025 22:38:53 -0500 Subject: [PATCH 237/354] Fix attachment is_spoiler() and is_voice_message() --- discord/message.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index e7752fb8d893..547e9c433aed 100644 --- a/discord/message.py +++ b/discord/message.py @@ -254,11 +254,12 @@ def flags(self) -> AttachmentFlags: def is_spoiler(self) -> bool: """:class:`bool`: Whether this attachment contains a spoiler.""" - return self.filename.startswith('SPOILER_') + # The flag is technically always present but no harm to check both + return self.filename.startswith('SPOILER_') or self.flags.spoiler def is_voice_message(self) -> bool: """:class:`bool`: Whether this attachment is a voice message.""" - return self.duration is not None and 'voice-message' in self.url + return self.duration is not None and self.waveform is not None def __repr__(self) -> str: return f'' From cab4732b7ea8008ace32f9bd6285f7d4b3a99299 Mon Sep 17 00:00:00 2001 From: dolfies Date: Tue, 4 Mar 2025 02:07:51 -0500 Subject: [PATCH 238/354] Make embed flags required and add them to all media fields --- discord/embeds.py | 64 ++++++++++++++++++++++-------------------- discord/types/embed.py | 22 +++------------ 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index 4be644688ee6..b35582b9f4ab 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -77,12 +77,7 @@ class _EmbedMediaProxy(Protocol): proxy_url: Optional[str] height: Optional[int] width: Optional[int] - flags: Optional[AttachmentFlags] - - class _EmbedVideoProxy(Protocol): - url: Optional[str] - height: Optional[int] - width: Optional[int] + flags: AttachmentFlags class _EmbedProviderProxy(Protocol): name: Optional[str] @@ -148,10 +143,6 @@ class Embed: colour: Optional[Union[:class:`Colour`, :class:`int`]] The colour code of the embed. Aliased to ``color`` as well. This can be set during initialisation. - flags: Optional[:class:`EmbedFlags`] - The flags of this embed. - - .. versionadded:: 2.5 """ __slots__ = ( @@ -168,7 +159,7 @@ class Embed: '_author', '_fields', 'description', - 'flags', + '_flags', ) def __init__( @@ -188,7 +179,7 @@ def __init__( self.type: EmbedType = type self.url: Optional[str] = url self.description: Optional[str] = description - self.flags: Optional[EmbedFlags] = None + self._flags: int = 0 if self.title is not None: self.title = str(self.title) @@ -223,6 +214,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: self.type = data.get('type', None) self.description = data.get('description', None) self.url = data.get('url', None) + self._flags = data.get('flags', 0) if self.title is not None: self.title = str(self.title) @@ -253,11 +245,6 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self: else: setattr(self, '_' + attr, value) - try: - self.flags = EmbedFlags._from_value(data['flags']) - except KeyError: - pass - return self def copy(self) -> Self: @@ -318,8 +305,17 @@ def __eq__(self, other: Embed) -> bool: and self.image == other.image and self.provider == other.provider and self.video == other.video + and self._flags == other._flags ) + @property + def flags(self) -> EmbedFlags: + """:class:`EmbedFlags`: The flags of this embed. + + .. versionadded:: 2.5 + """ + return EmbedFlags._from_value(self._flags or 0) + @property def colour(self) -> Optional[Colour]: return getattr(self, '_colour', None) @@ -408,18 +404,17 @@ def image(self) -> _EmbedMediaProxy: Possible attributes you can access are: - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` - - ``flags`` + - ``url`` for the image URL. + - ``proxy_url`` for the proxied image URL. + - ``width`` for the image width. + - ``height`` for the image height. + - ``flags`` for the image's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. data = getattr(self, '_image', {}) - if 'flags' in data: - data['flags'] = AttachmentFlags._from_value(data['flags']) + data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) return EmbedProxy(data) # type: ignore def set_image(self, *, url: Optional[Any]) -> Self: @@ -454,15 +449,18 @@ def thumbnail(self) -> _EmbedMediaProxy: Possible attributes you can access are: - - ``url`` - - ``proxy_url`` - - ``width`` - - ``height`` + - ``url`` for the thumbnail URL. + - ``proxy_url`` for the proxied thumbnail URL. + - ``width`` for the thumbnail width. + - ``height`` for the thumbnail height. + - ``flags`` for the thumbnail's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore + data = getattr(self, '_thumbnail', {}) + data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) + return EmbedProxy(data) # type: ignore def set_thumbnail(self, *, url: Optional[Any]) -> Self: """Sets the thumbnail for the embed content. @@ -491,19 +489,23 @@ def set_thumbnail(self, *, url: Optional[Any]) -> Self: return self @property - def video(self) -> _EmbedVideoProxy: + def video(self) -> _EmbedMediaProxy: """Returns an ``EmbedProxy`` denoting the video contents. Possible attributes include: - ``url`` for the video URL. + - ``proxy_url`` for the proxied video URL. - ``height`` for the video height. - ``width`` for the video width. + - ``flags`` for the video's attachment flags. If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - return EmbedProxy(getattr(self, '_video', {})) # type: ignore + data = getattr(self, '_video', {}) + data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) + return EmbedProxy(data) # type: ignore @property def provider(self) -> _EmbedProviderProxy: diff --git a/discord/types/embed.py b/discord/types/embed.py index f8354a3f3069..a18912f6e392 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -38,28 +38,14 @@ class EmbedField(TypedDict): inline: NotRequired[bool] -class EmbedThumbnail(TypedDict, total=False): +class EmbedMedia(TypedDict, total=False): url: Required[str] proxy_url: str height: int width: int - - -class EmbedVideo(TypedDict, total=False): - url: str - proxy_url: str - height: int - width: int flags: int -class EmbedImage(TypedDict, total=False): - url: Required[str] - proxy_url: str - height: int - width: int - - class EmbedProvider(TypedDict, total=False): name: str url: str @@ -83,9 +69,9 @@ class Embed(TypedDict, total=False): timestamp: str color: int footer: EmbedFooter - image: EmbedImage - thumbnail: EmbedThumbnail - video: EmbedVideo + image: EmbedMedia + thumbnail: EmbedMedia + video: EmbedMedia provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] From 6b0a6eea6637c5c44000aea7d8c584bf1862a4d1 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 02:13:31 -0500 Subject: [PATCH 239/354] Add v2.5.1 changelog --- docs/whats_new.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index d3763a588e0f..975567c6cd14 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,20 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p5p1: + +v2.5.1 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix :attr:`InteractionCallbackResponse.resource` having incorrect state (:issue:`10107`) +- Create :class:`ScheduledEvent` on cache miss for :func:`on_scheduled_event_delete` (:issue:`10113`) +- Add defaults for :class:`Message` creation preventing some crashes (:issue:`10115`) +- Fix :meth:`Attachment.is_spoiler` and :meth:`Attachment.is_voice_message` being incorrect (:issue:`10122`) + + .. _vp2p5p0: v2.5.0 @@ -63,7 +77,6 @@ New Features - Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`) - Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`) -- Add - Add richer :meth:`Role.move` interface (:issue:`10100`) - Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`) - Add new flags for :class:`AttachmentFlags` (:issue:`10085`) From 73f261d536715af5559059268c26515812e51be7 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 02:14:02 -0500 Subject: [PATCH 240/354] Version bump to v2.5.1 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 48fe1092541e..56d503fc4065 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.6.0a' +__version__ = '2.5.1' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=1, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 973bb5089ffa60f2db5244aca52b1c6cab43661f Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 02:15:32 -0500 Subject: [PATCH 241/354] Version bump for development --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 56d503fc4065..48fe1092541e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.5.1' +__version__ = '2.6.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=1, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 18f72f58fd5111b60d8d1879d2704cfe34aeaa76 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:33:55 +0100 Subject: [PATCH 242/354] idk some things --- discord/ui/action_row.py | 292 +++++++++++++++++++++++++++++++++++++++ discord/ui/button.py | 2 + discord/ui/select.py | 2 + discord/ui/view.py | 133 ++++++++++++------ 4 files changed, 384 insertions(+), 45 deletions(-) create mode 100644 discord/ui/action_row.py diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py new file mode 100644 index 000000000000..160a9eca812c --- /dev/null +++ b/discord/ui/action_row.py @@ -0,0 +1,292 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +import inspect +import os +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Coroutine, + Dict, + List, + Literal, + Optional, + Sequence, + Type, + TypeVar, + Union, +) + +from .item import Item, ItemCallbackType +from .button import Button +from .select import Select, SelectCallbackDecorator +from ..enums import ButtonStyle, ComponentType, ChannelType +from ..partial_emoji import PartialEmoji +from ..utils import MISSING + +if TYPE_CHECKING: + from .view import LayoutView + from .select import ( + BaseSelectT, + ValidDefaultValues, + MentionableSelectT, + ChannelSelectT, + RoleSelectT, + UserSelectT, + SelectT + ) + from ..emoji import Emoji + from ..components import SelectOption + from ..interactions import Interaction + +V = TypeVar('V', bound='LayoutView', covariant=True) + +__all__ = ('ActionRow',) + + +class _ActionRowCallback: + __slots__ = ('row', 'callback', 'item') + + def __init__(self, callback: ItemCallbackType[Any, Any], row: ActionRow, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any, Any] = callback + self.row: ActionRow = row + self.item: Item[Any] = item + + def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: + return self.callback(self.row, interaction, self.item) + + +class ActionRow(Item[V]): + """Represents a UI action row. + + This object can be inherited. + + .. versionadded:: 2.6 + + Parameters + ---------- + id: Optional[:class:`str`] + The ID of this action row. Defaults to ``None``. + """ + + __action_row_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] + __discord_ui_action_row__: ClassVar[bool] = True + + def __init__(self, *, id: Optional[str] = None) -> None: + super().__init__() + + self.id: str = id or os.urandom(16).hex() + self._children: List[Item[Any]] = self._init_children() + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, ItemCallbackType[Any, Any]] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if hasattr(member, '__discord_ui_model_type__'): + children[name] = member + + if len(children) > 5: + raise TypeError('ActionRow cannot have more than 5 children') + + cls.__action_row_children_items__ = list(children.values()) + + def _init_children(self) -> List[Item[Any]]: + children = [] + + for func in self.__action_row_children_items__: + item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item.callback = _ActionRowCallback(func, self, item) + item._parent = self # type: ignore + setattr(self, func.__name__, item) + children.append(item) + return children + + def _update_children_view(self, view: LayoutView) -> None: + for child in self._children: + child._view = view + + def _is_v2(self) -> bool: + # although it is not really a v2 component the only usecase here is for + # LayoutView which basically represents the top-level payload of components + # and ActionRow is only allowed there anyways. + # If the user tries to add any V2 component to a View instead of LayoutView + # it should error anyways. + return True + + @property + def width(self): + return 5 + + @property + def type(self) -> Literal[ComponentType.action_row]: + return ComponentType.action_row + + def button( + self, + *, + label: Optional[str] = None, + custom_id: Optional[str] = None, + disabled: bool = False, + style: ButtonStyle = ButtonStyle.secondary, + emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + ) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: + """A decorator that attaches a button to a component. + + The function being decorated should have three parameters, ``self`` representing + the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and + the :class:`discord.ui.Button` being pressed. + + .. note:: + + Buttons with a URL or a SKU cannot be created with this function. + Consider creating a :class:`Button` manually and adding it via + :meth:`ActionRow.add_item` instead. This is beacuse these buttons + cannot have a callback associated with them since Discord does not + do any processing with them. + + Parameters + ---------- + label: Optional[:class:`str`] + The label of the button, if any. + Can only be up to 80 characters. + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. + It is recommended to not set this parameters to prevent conflicts. + Can only be up to 100 characters. + style: :class:`.ButtonStyle` + The style of the button. Defaults to :attr:`.ButtonStyle.grey`. + disabled: :class:`bool` + Whether the button is disabled or not. Defaults to ``False``. + emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] + The emoji of the button. This can be in string form or a :class:`.PartialEmoji` + or a full :class:`.Emoji`. + """ + + def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: + if not inspect.iscoroutinefunction(func): + raise TypeError('button function must be a coroutine function') + + func.__discord_ui_modal_type__ = Button + func.__discord_ui_model_kwargs__ = { + 'style': style, + 'custom_id': custom_id, + 'url': None, + 'disabled': disabled, + 'label': label, + 'emoji': emoji, + 'row': None, + 'sku_id': None, + } + return func + + return decorator # type: ignore + + def select( + *, + cls: Type[BaseSelectT] = Select[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = None, + custom_id: str = MISSING, + min_values: int = 1, + max_values: int = 1, + disabled: bool = False, + default_values: Sequence[ValidDefaultValues] = MISSING, + ) -> SelectCallbackDecorator[V, BaseSelectT]: + """A decorator that attaches a select menu to a component. + + The function being decorated should have three parameters, ``self`` representing + the :class:`discord.ui.View`, the :class:`discord.Interaction` you receive and + the chosen select class. + + To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values + will depend on the type of select menu used. View the table below for more information. + + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | Select Type | Resolved Values | + +========================================+=================================================================================================================+ + | :class:`discord.ui.Select` | List[:class:`str`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.UserSelect` | List[Union[:class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.RoleSelect` | List[:class:`discord.Role`] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.MentionableSelect` | List[Union[:class:`discord.Role`, :class:`discord.Member`, :class:`discord.User`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + | :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] | + +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ + + .. versionchanged:: 2.1 + Added the following keyword-arguments: ``cls``, ``channel_types`` + + Example + --------- + .. code-block:: python3 + + class ActionRow(discord.ui.ActionRow): + + @discord.ui.select(cls=ChannelSelect, channel_types=[discord.ChannelType.text]) + async def select_channels(self, interaction: discord.Interaction, select: ChannelSelect): + return await interaction.response.send_message(f'You selected {select.values[0].mention}') + + Parameters + ------------ + cls: Union[Type[:class:`discord.ui.Select`], Type[:class:`discord.ui.UserSelect`], Type[:class:`discord.ui.RoleSelect`], \ + Type[:class:`discord.ui.MentionableSelect`], Type[:class:`discord.ui.ChannelSelect`]] + The class to use for the select menu. Defaults to :class:`discord.ui.Select`. You can use other + select types to display different select menus to the user. See the table above for the different + values you can get from each select type. Subclasses work as well, however the callback in the subclass will + get overridden. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + Can only be up to 150 characters. + custom_id: :class:`str` + The ID of the select menu that gets received during an interaction. + It is recommended not to set this parameter to prevent conflicts. + Can only be up to 100 characters. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`discord.SelectOption`] + A list of options that can be selected in this menu. This can only be used with + :class:`Select` instances. + Can only contain up to 25 items. + channel_types: List[:class:`~discord.ChannelType`] + The types of channels to show in the select menu. Defaults to all channels. This can only be used + with :class:`ChannelSelect` instances. + disabled: :class:`bool` + Whether the select is disabled or not. Defaults to ``False``. + default_values: Sequence[:class:`~discord.abc.Snowflake`] + A list of objects representing the default values for the select menu. This cannot be used with regular :class:`Select` instances. + If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. + Number of items must be in range of ``min_values`` and ``max_values``. + """ diff --git a/discord/ui/button.py b/discord/ui/button.py index 43bd3a8b0f9d..f15910effce1 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -43,6 +43,7 @@ from typing_extensions import Self from .view import View + from .action_row import ActionRow from ..emoji import Emoji from ..types.components import ButtonComponent as ButtonComponentPayload @@ -144,6 +145,7 @@ def __init__( emoji=emoji, sku_id=sku_id, ) + self._parent: Optional[ActionRow] = None self.row = row @property diff --git a/discord/ui/select.py b/discord/ui/select.py index 1ef085cc5df2..b2534e146f85 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -73,6 +73,7 @@ from typing_extensions import TypeAlias, TypeGuard from .view import View + from .action_row import ActionRow from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import SelectMessageComponentInteractionData from ..app_commands import AppCommandChannel, AppCommandThread @@ -258,6 +259,7 @@ def __init__( ) self.row = row + self._parent: Optional[ActionRow] = None self._values: List[PossibleValue] = [] @property diff --git a/discord/ui/view.py b/discord/ui/view.py index e490f1444cee..e19f8bc6c684 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -36,7 +36,6 @@ TYPE_CHECKING, Tuple, Type, - Union, ) from functools import partial from itertools import groupby @@ -47,6 +46,7 @@ import time import os from .item import Item, ItemCallbackType +from .action_row import ActionRow from .dynamic import DynamicItem from ..components import ( Component, @@ -153,9 +153,6 @@ def find_open_space(self, item: Item) -> int: raise ValueError('could not find open space for item') def add_item(self, item: Item) -> None: - if item._is_v2() and not self.v2_weights(): - # v2 components allow up to 10 rows - self.weights.extend([0, 0, 0, 0, 0]) if item.row is not None: total = self.weights[item.row] + item.width if total > 5: @@ -191,7 +188,7 @@ def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: return self.callback(self.view, interaction, self.item) -class View: +class View: # NOTE: maybe add a deprecation warning in favour of LayoutView? """Represents a UI view. This object must be inherited to create a UI within Discord. @@ -208,15 +205,15 @@ class View: __discord_ui_view__: ClassVar[bool] = True __discord_ui_modal__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False - __view_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] + __view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] def __init_subclass__(cls) -> None: super().__init_subclass__() - children: Dict[str, Union[ItemCallbackType[Any, Any], Item]] = {} + children: Dict[str, ItemCallbackType[Any, Any]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): - if hasattr(member, '__discord_ui_model_type__') or isinstance(member, Item): + if hasattr(member, '__discord_ui_model_type__'): children[name] = member if len(children) > 25: @@ -228,15 +225,11 @@ def _init_children(self) -> List[Item[Self]]: children = [] for func in self.__view_children_items__: - if isinstance(func, Item): - func._view = self - children.append(func) - else: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) # type: ignore - item._view = self - setattr(self, func.__name__, item) - children.append(item) + item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item.callback = _ViewCallback(func, self, item) # type: ignore + item._view = self + setattr(self, func.__name__, item) + children.append(item) return children def __init__(self, *, timeout: Optional[float] = 180.0): @@ -286,36 +279,22 @@ def has_components_v2(self) -> bool: return any(c._is_v2() for c in self.children) def to_components(self) -> List[Dict[str, Any]]: - components: List[Dict[str, Any]] = [] - rows_index: Dict[int, int] = {} - # helper mapping to find action rows for items that are not - # v2 components - def key(item: Item) -> int: return item._rendered_row or 0 - # instead of grouping by row we will sort it so it is added - # in order and should work as the original implementation - # this will append directly the v2 Components into the list - # and will add to an action row the loose items, such as - # buttons and selects - for child in sorted(self._children, key=key): - if child._is_v2(): - components.append(child.to_component_dict()) - else: - row = child._rendered_row or 0 - index = rows_index.get(row) + children = sorted(self._children, key=key) + components: List[Dict[str, Any]] = [] + for _, group in groupby(children, key=key): + children = [item.to_component_dict() for item in group] + if not children: + continue - if index is not None: - components[index]['components'].append(child.to_component_dict()) - else: - components.append( - { - 'type': 1, - 'components': [child.to_component_dict()], - }, - ) - rows_index[row] = len(components) - 1 + components.append( + { + 'type': 1, + 'components': children, + } + ) return components @@ -401,8 +380,9 @@ def add_item(self, item: Item[Any]) -> Self: TypeError An :class:`Item` was not passed. ValueError - Maximum number of children has been exceeded (25) - or the row the item is trying to be added to is full. + Maximum number of children has been exceeded (25), the + row the item is trying to be added to is full or the item + you tried to add is not allowed in this View. """ if len(self._children) >= 25: @@ -411,6 +391,11 @@ def add_item(self, item: Item[Any]) -> Self: if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') + if item._is_v2() and not self._is_v2(): + raise ValueError( + 'The item can only be added on LayoutView' + ) + self.__weights.add_item(item) item._view = self @@ -614,6 +599,64 @@ async def wait(self) -> bool: return await self.__stopped +class LayoutView(View): + __view_children_items__: ClassVar[List[Item[Any]]] = [] + + def __init__(self, *, timeout: Optional[float] = 180) -> None: + super().__init__(timeout=timeout) + self.__weights.weights.extend([0, 0, 0, 0, 0]) + + def __init_subclass__(cls) -> None: + children: Dict[str, Item[Any]] = {} + + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if isinstance(member, Item): + children[name] = member + + if len(children) > 10: + raise TypeError('LayoutView cannot have more than 10 top-level children') + + cls.__view_children_items__ = list(children.values()) + + def _init_children(self) -> List[Item[Self]]: + children = [] + + for i in self.__view_children_items__: + if isinstance(i, Item): + if getattr(i, '_parent', None): + # this is for ActionRows which have decorators such as + # @action_row.button and @action_row.select that will convert + # those callbacks into their types but will have a _parent + # attribute which is checked here so the item is not added twice + continue + i._view = self + if getattr(i, '__discord_ui_action_row__', False): + i._update_children_view(self) # type: ignore + children.append(i) + else: + # guard just in case + raise TypeError( + 'LayoutView can only have items' + ) + return children + + def _is_v2(self) -> bool: + return True + + def to_components(self): + components: List[Dict[str, Any]] = [] + + # sorted by row, which in LayoutView indicates the position of the component in the + # payload instead of in which ActionRow it should be placed on. + for child in sorted(self._children, key=lambda i: i._rendered_row or 0): + components.append( + child.to_component_dict(), + ) + + return child + + class ViewStore: def __init__(self, state: ConnectionState): # entity_id: {(component_type, custom_id): Item} From 2f8b2624f121653841bbc69651979baac6f59cd3 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 4 Mar 2025 23:42:28 +0100 Subject: [PATCH 243/354] Fix improper class in audit log docs --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 73e0238fcd6a..e366f63bf312 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2418,7 +2418,7 @@ of :class:`enum.Enum`. When this is the action, the type of :attr:`~AuditLogEntry.extra` is set to an unspecified proxy object with two attributes: - - ``channel``: A :class:`TextChannel` or :class:`Object` with the channel ID where the members were moved. + - ``channel``: An :class:`abc.Connectable` or :class:`Object` with the channel ID where the members were moved. - ``count``: An integer specifying how many members were moved. .. versionadded:: 1.3 From 8594dd1b309fd66bc2defc1f73540dfa9357e2c7 Mon Sep 17 00:00:00 2001 From: dolfies Date: Tue, 4 Mar 2025 18:24:38 -0500 Subject: [PATCH 244/354] Fix embed media flags regression --- discord/embeds.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index b35582b9f4ab..7f84e410d341 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -46,7 +46,7 @@ def __len__(self) -> int: return len(self.__dict__) def __repr__(self) -> str: - inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_'))) + inner = ', '.join((f'{k}={getattr(self, k)!r}' for k in dir(self) if not k.startswith('_'))) return f'EmbedProxy({inner})' def __getattr__(self, attr: str) -> None: @@ -56,6 +56,16 @@ def __eq__(self, other: object) -> bool: return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__ +class EmbedMediaProxy(EmbedProxy): + def __init__(self, layer: Dict[str, Any]): + super().__init__(layer) + self._flags = self.__dict__.pop('flags', 0) + + @property + def flags(self) -> AttachmentFlags: + return AttachmentFlags._from_value(self._flags or 0) + + if TYPE_CHECKING: from typing_extensions import Self @@ -413,9 +423,7 @@ def image(self) -> _EmbedMediaProxy: If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - data = getattr(self, '_image', {}) - data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) - return EmbedProxy(data) # type: ignore + return EmbedMediaProxy(getattr(self, '_image', {})) # type: ignore def set_image(self, *, url: Optional[Any]) -> Self: """Sets the image for the embed content. @@ -458,9 +466,7 @@ def thumbnail(self) -> _EmbedMediaProxy: If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - data = getattr(self, '_thumbnail', {}) - data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) - return EmbedProxy(data) # type: ignore + return EmbedMediaProxy(getattr(self, '_thumbnail', {})) # type: ignore def set_thumbnail(self, *, url: Optional[Any]) -> Self: """Sets the thumbnail for the embed content. @@ -503,9 +509,7 @@ def video(self) -> _EmbedMediaProxy: If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. - data = getattr(self, '_video', {}) - data['flags'] = AttachmentFlags._from_value(data.get('flags', 0)) - return EmbedProxy(data) # type: ignore + return EmbedMediaProxy(getattr(self, '_video', {})) # type: ignore @property def provider(self) -> _EmbedProviderProxy: From f4bce1caf02125b9ddad25a967358756a677add9 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 20:12:54 -0500 Subject: [PATCH 245/354] Add changelog for v2.5.2 --- docs/whats_new.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 975567c6cd14..44db8c3d4042 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,16 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p5p2: + +v2.5.2 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix a serialization issue when sending embeds (:issue:`10126`) + .. _vp2p5p1: v2.5.1 From d2a6ccf715b0d7d53ef75ebc767d5696b9c967a9 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 20:13:27 -0500 Subject: [PATCH 246/354] Version bump to v2.5.2 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 48fe1092541e..0efb6553bd27 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.6.0a' +__version__ = '2.5.2' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=2, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 460d188359ff43419b74a15b3376095d1443d19c Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 4 Mar 2025 20:16:50 -0500 Subject: [PATCH 247/354] Version bump for development --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 0efb6553bd27..48fe1092541e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.5.2' +__version__ = '2.6.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -83,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=2, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 26855160f8a8f0dfade609cce6b1bc97f8b8fa14 Mon Sep 17 00:00:00 2001 From: Rishit Khare Date: Wed, 5 Mar 2025 09:30:57 -0600 Subject: [PATCH 248/354] update PyNaCl minimum version dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7360731df31..92ccb738106d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ Documentation = "https://discordpy.readthedocs.io/en/latest/" dependencies = { file = "requirements.txt" } [project.optional-dependencies] -voice = ["PyNaCl>=1.3.0,<1.6"] +voice = ["PyNaCl>=1.5.0,<1.6"] docs = [ "sphinx==4.4.0", "sphinxcontrib_trio==1.1.2", From cbdc618e3e5f4c49d317acb35086ec7d0bb80133 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:16:24 +0100 Subject: [PATCH 249/354] chore: Finish ActionRow and fix ViewStore.add_view --- discord/ui/action_row.py | 198 ++++++++++++++++++++++++++++++++++++++- discord/ui/view.py | 19 ++++ 2 files changed, 212 insertions(+), 5 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 160a9eca812c..4101eb2ddab0 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -39,16 +39,20 @@ Type, TypeVar, Union, + overload, ) from .item import Item, ItemCallbackType from .button import Button -from .select import Select, SelectCallbackDecorator +from .dynamic import DynamicItem +from .select import select as _select, Select, SelectCallbackDecorator, UserSelect, RoleSelect, ChannelSelect, MentionableSelect from ..enums import ButtonStyle, ComponentType, ChannelType from ..partial_emoji import PartialEmoji from ..utils import MISSING if TYPE_CHECKING: + from typing_extensions import Self + from .view import LayoutView from .select import ( BaseSelectT, @@ -122,11 +126,26 @@ def _init_children(self) -> List[Item[Any]]: for func in self.__action_row_children_items__: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item.callback = _ActionRowCallback(func, self, item) - item._parent = self # type: ignore + item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore setattr(self, func.__name__, item) children.append(item) return children + def _update_store_data(self, dispatch_info: Dict, dynamic_items: Dict) -> bool: + is_fully_dynamic = True + + for item in self._children: + if isinstance(item, DynamicItem): + pattern = item.__discord_ui_compiled_template__ + dynamic_items[pattern] = item.__class__ + elif item.is_dispatchable(): + dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + is_fully_dynamic = False + return is_fully_dynamic + + def is_dispatchable(self) -> bool: + return any(c.is_dispatchable() for c in self.children) + def _update_children_view(self, view: LayoutView) -> None: for child in self._children: child._view = view @@ -147,6 +166,77 @@ def width(self): def type(self) -> Literal[ComponentType.action_row]: return ComponentType.action_row + @property + def children(self) -> List[Item[V]]: + """List[:class:`Item`]: The list of children attached to this action row.""" + return self._children.copy() + + def add_item(self, item: Item[Any]) -> Self: + """Adds an item to this row. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to add to the row. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of children has been exceeded (5). + """ + + if len(self._children) >= 5: + raise ValueError('maximum number of children exceeded') + + if not isinstance(item, Item): + raise TypeError(f'expected Item not {item.__class__.__name__}') + + self._children.append(item) + return self + + def remove_item(self, item: Item[Any]) -> Self: + """Removes an item from the row. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to remove from the view. + """ + + try: + self._children.remove(item) + except ValueError: + pass + return self + + def clear_items(self) -> Self: + """Removes all items from the row. + + This function returns the class instance to allow for fluent-style + chaining. + """ + self._children.clear() + return self + + def to_component_dict(self) -> Dict[str, Any]: + components = [] + + for item in self._children: + components.append(item.to_component_dict()) + + return { + 'type': self.type.value, + 'components': components, + } + def button( self, *, @@ -192,6 +282,7 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto if not inspect.iscoroutinefunction(func): raise TypeError('button function must be a coroutine function') + func.__discord_ui_parent__ = self func.__discord_ui_modal_type__ = Button func.__discord_ui_model_kwargs__ = { 'style': style, @@ -207,7 +298,90 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto return decorator # type: ignore + @overload def select( + self, + *, + cls: Type[SelectT] = Select[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + ) -> SelectCallbackDecorator[V, SelectT]: + ... + + @overload + def select( + self, + *, + cls: Type[UserSelectT] = UserSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[V, UserSelectT]: + ... + + + @overload + def select( + self, + *, + cls: Type[RoleSelectT] = RoleSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[V, RoleSelectT]: + ... + + + @overload + def select( + self, + *, + cls: Type[ChannelSelectT] = ChannelSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = ..., + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[V, ChannelSelectT]: + ... + + + @overload + def select( + self, + *, + cls: Type[MentionableSelectT] = MentionableSelect[Any], + options: List[SelectOption] = MISSING, + channel_types: List[ChannelType] = MISSING, + placeholder: Optional[str] = ..., + custom_id: str = ..., + min_values: int = ..., + max_values: int = ..., + disabled: bool = ..., + default_values: Sequence[ValidDefaultValues] = ..., + ) -> SelectCallbackDecorator[V, MentionableSelectT]: + ... + + def select( + self, *, cls: Type[BaseSelectT] = Select[Any], options: List[SelectOption] = MISSING, @@ -242,9 +416,6 @@ def select( | :class:`discord.ui.ChannelSelect` | List[Union[:class:`~discord.app_commands.AppCommandChannel`, :class:`~discord.app_commands.AppCommandThread`]] | +----------------------------------------+-----------------------------------------------------------------------------------------------------------------+ - .. versionchanged:: 2.1 - Added the following keyword-arguments: ``cls``, ``channel_types`` - Example --------- .. code-block:: python3 @@ -290,3 +461,20 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe If ``cls`` is :class:`MentionableSelect` and :class:`.Object` is passed, then the type must be specified in the constructor. Number of items must be in range of ``min_values`` and ``max_values``. """ + + def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: + r = _select( # type: ignore + cls=cls, # type: ignore + placeholder=placeholder, + custom_id=custom_id, + min_values=min_values, + max_values=max_values, + options=options, + channel_types=channel_types, + disabled=disabled, + default_values=default_values, + )(func) + r.__discord_ui_parent__ = self + return r + + return decorator # type: ignore diff --git a/discord/ui/view.py b/discord/ui/view.py index e19f8bc6c684..9ea612aebe64 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -601,6 +601,7 @@ async def wait(self) -> bool: class LayoutView(View): __view_children_items__: ClassVar[List[Item[Any]]] = [] + __view_pending_children__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] def __init__(self, *, timeout: Optional[float] = 180) -> None: super().__init__(timeout=timeout) @@ -608,20 +609,32 @@ def __init__(self, *, timeout: Optional[float] = 180) -> None: def __init_subclass__(cls) -> None: children: Dict[str, Item[Any]] = {} + pending: Dict[str, ItemCallbackType[Any, Any]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if isinstance(member, Item): children[name] = member + elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): + pending[name] = member if len(children) > 10: raise TypeError('LayoutView cannot have more than 10 top-level children') cls.__view_children_items__ = list(children.values()) + cls.__view_pending_children__ = list(pending.values()) def _init_children(self) -> List[Item[Self]]: children = [] + for func in self.__view_pending_children__: + item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item.callback = _ViewCallback(func, self, item) + item._view = self + setattr(self, func.__name__, item) + parent: ActionRow = func.__discord_ui_parent__ + parent.add_item(item) + for i in self.__view_children_items__: if isinstance(i, Item): if getattr(i, '_parent', None): @@ -639,6 +652,7 @@ def _init_children(self) -> List[Item[Self]]: raise TypeError( 'LayoutView can only have items' ) + return children def _is_v2(self) -> bool: @@ -709,6 +723,11 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: dispatch_info, self._dynamic_items, ) + elif getattr(item, '__discord_ui_action_row__', False): + is_fully_dynamic = item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) or is_fully_dynamic else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 8f59216e680137895104c6ee5e873c2b35f195fd Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:31:26 +0100 Subject: [PATCH 250/354] chore: more components v2 things and finished danny's suggested impl --- discord/components.py | 2 +- discord/ui/action_row.py | 16 +- discord/ui/button.py | 5 +- discord/ui/container.py | 8 +- discord/ui/dynamic.py | 8 +- discord/ui/file.py | 8 +- discord/ui/item.py | 16 +- discord/ui/media_gallery.py | 15 +- discord/ui/section.py | 8 +- discord/ui/select.py | 5 +- discord/ui/separator.py | 8 +- discord/ui/text_display.py | 9 +- discord/ui/thumbnail.py | 8 +- discord/ui/view.py | 397 +++++++++++++++++++++++------------- 14 files changed, 336 insertions(+), 177 deletions(-) diff --git a/discord/components.py b/discord/components.py index ac4ff987e51e..ef7d676700ac 100644 --- a/discord/components.py +++ b/discord/components.py @@ -967,7 +967,7 @@ def __repr__(self) -> str: def _from_data(cls, data: MediaGalleryItemPayload, state: Optional[ConnectionState]) -> MediaGalleryItem: media = data['media'] self = cls( - media=media['url'], + media=UnfurledMediaItem._from_data(media, state), description=data.get('description'), spoiler=data.get('spoiler', False), ) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 4101eb2ddab0..1df526cba41c 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -45,7 +45,8 @@ from .item import Item, ItemCallbackType from .button import Button from .dynamic import DynamicItem -from .select import select as _select, Select, SelectCallbackDecorator, UserSelect, RoleSelect, ChannelSelect, MentionableSelect +from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect +from ..components import ActionRow as ActionRowComponent from ..enums import ButtonStyle, ComponentType, ChannelType from ..partial_emoji import PartialEmoji from ..utils import MISSING @@ -61,7 +62,8 @@ ChannelSelectT, RoleSelectT, UserSelectT, - SelectT + SelectT, + SelectCallbackDecorator, ) from ..emoji import Emoji from ..components import SelectOption @@ -125,7 +127,7 @@ def _init_children(self) -> List[Item[Any]]: for func in self.__action_row_children_items__: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ActionRowCallback(func, self, item) + item.callback = _ActionRowCallback(func, self, item) # type: ignore item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore setattr(self, func.__name__, item) children.append(item) @@ -478,3 +480,11 @@ def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, Bas return r return decorator # type: ignore + + @classmethod + def from_component(cls, component: ActionRowComponent) -> ActionRow: + from .view import _component_to_item + self = cls() + for cmp in component.children: + self.add_item(_component_to_item(cmp)) + return self diff --git a/discord/ui/button.py b/discord/ui/button.py index f15910effce1..df21c770fc4b 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -42,12 +42,12 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import BaseView from .action_row import ActionRow from ..emoji import Emoji from ..types.components import ButtonComponent as ButtonComponentPayload -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='BaseView', covariant=True) class Button(Item[V]): @@ -147,6 +147,7 @@ def __init__( ) self._parent: Optional[ActionRow] = None self.row = row + self.id = custom_id @property def style(self) -> ButtonStyle: diff --git a/discord/ui/container.py b/discord/ui/container.py index 2acf95d20bad..1b50eceb9c32 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -26,7 +26,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar from .item import Item -from .view import View, _component_to_item +from .view import View, _component_to_item, LayoutView from .dynamic import DynamicItem from ..enums import ComponentType from ..utils import MISSING @@ -37,7 +37,7 @@ from ..colour import Colour, Color from ..components import Container as ContainerComponent -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('Container',) @@ -69,6 +69,8 @@ class Container(View, Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ __discord_ui_container__ = True @@ -82,6 +84,7 @@ def __init__( spoiler: bool = False, timeout: Optional[float] = 180, row: Optional[int] = None, + id: Optional[str] = None, ) -> None: super().__init__(timeout=timeout) if children is not MISSING: @@ -95,6 +98,7 @@ def __init__( self._row: Optional[int] = None self._rendered_row: Optional[int] = None self.row: Optional[int] = row + self.id: Optional[str] = id @property def children(self) -> List[Item[Self]]: diff --git a/discord/ui/dynamic.py b/discord/ui/dynamic.py index 0b65e90f3a35..ee3ad30d50c1 100644 --- a/discord/ui/dynamic.py +++ b/discord/ui/dynamic.py @@ -38,14 +38,14 @@ from ..interactions import Interaction from ..components import Component from ..enums import ComponentType - from .view import View + from .view import BaseView - V = TypeVar('V', bound='View', covariant=True, default=View) + V = TypeVar('V', bound='BaseView', covariant=True, default=BaseView) else: - V = TypeVar('V', bound='View', covariant=True) + V = TypeVar('V', bound='BaseView', covariant=True) -class DynamicItem(Generic[BaseT], Item['View']): +class DynamicItem(Generic[BaseT], Item['BaseView']): """Represents an item with a dynamic ``custom_id`` that can be used to store state within that ``custom_id``. diff --git a/discord/ui/file.py b/discord/ui/file.py index 3ff6c7d0f04f..2654d351c305 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -32,9 +32,9 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('File',) @@ -59,6 +59,8 @@ class File(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ def __init__( @@ -67,6 +69,7 @@ def __init__( *, spoiler: bool = False, row: Optional[int] = None, + id: Optional[str] = None, ) -> None: super().__init__() self._underlying = FileComponent._raw_construct( @@ -75,6 +78,7 @@ def __init__( ) self.row = row + self.id = id def _is_v2(self): return True diff --git a/discord/ui/item.py b/discord/ui/item.py index bbd90464a603..1fa68b68c701 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -37,11 +37,11 @@ if TYPE_CHECKING: from ..enums import ComponentType - from .view import View + from .view import BaseView from ..components import Component I = TypeVar('I', bound='Item[Any]') -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='BaseView', covariant=True) ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] @@ -70,6 +70,7 @@ def __init__(self): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False + self._id: Optional[str] = None self._max_row: int = 5 if not self._is_v2() else 10 def to_component_dict(self) -> Dict[str, Any]: @@ -124,6 +125,17 @@ def view(self) -> Optional[V]: """Optional[:class:`View`]: The underlying view for this item.""" return self._view + @property + def id(self) -> Optional[str]: + """Optional[:class:`str`]: The ID of this component. For non v2 components this is the + equivalent to ``custom_id``. + """ + return self._id + + @id.setter + def id(self, value: Optional[str]) -> None: + self._id = value + async def callback(self, interaction: Interaction[ClientT]) -> Any: """|coro| diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 4bc6c826f630..f9e1fb2644c7 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -35,9 +35,9 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('MediaGallery',) @@ -60,9 +60,17 @@ class MediaGallery(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ - def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) -> None: + def __init__( + self, + items: List[MediaGalleryItem], + *, + row: Optional[int] = None, + id: Optional[str] = None, + ) -> None: super().__init__() self._underlying = MediaGalleryComponent._raw_construct( @@ -70,6 +78,7 @@ def __init__(self, items: List[MediaGalleryItem], *, row: Optional[int] = None) ) self.row = row + self.id = id @property def items(self) -> List[MediaGalleryItem]: diff --git a/discord/ui/section.py b/discord/ui/section.py index 5a0ec7f27398..ba919beb81b6 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -33,10 +33,10 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView from ..components import SectionComponent -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('Section',) @@ -59,6 +59,8 @@ class Section(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ __slots__ = ( @@ -72,6 +74,7 @@ def __init__( *, accessory: Item[Any], row: Optional[int] = None, + id: Optional[str] = None, ) -> None: super().__init__() self._children: List[Item[Any]] = [] @@ -84,6 +87,7 @@ def __init__( self.accessory: Item[Any] = accessory self.row = row + self.id = id @property def type(self) -> Literal[ComponentType.section]: diff --git a/discord/ui/select.py b/discord/ui/select.py index b2534e146f85..f5a9fcbee2e1 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -72,7 +72,7 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias, TypeGuard - from .view import View + from .view import BaseView from .action_row import ActionRow from ..types.components import SelectMenu as SelectMenuPayload from ..types.interactions import SelectMessageComponentInteractionData @@ -102,7 +102,7 @@ Thread, ] -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='BaseView', covariant=True) BaseSelectT = TypeVar('BaseSelectT', bound='BaseSelect[Any]') SelectT = TypeVar('SelectT', bound='Select[Any]') UserSelectT = TypeVar('UserSelectT', bound='UserSelect[Any]') @@ -259,6 +259,7 @@ def __init__( ) self.row = row + self.id = custom_id if custom_id is not MISSING else None self._parent: Optional[ActionRow] = None self._values: List[PossibleValue] = [] diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 33401f880b95..b9ff955adcc5 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -32,9 +32,9 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('Separator',) @@ -58,6 +58,8 @@ class Separator(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ def __init__( @@ -66,6 +68,7 @@ def __init__( visible: bool = True, spacing: SeparatorSize = SeparatorSize.small, row: Optional[int] = None, + id: Optional[str] = None, ) -> None: super().__init__() self._underlying = SeparatorComponent._raw_construct( @@ -74,6 +77,7 @@ def __init__( ) self.row = row + self.id = id def _is_v2(self): return True diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 1bf88678d798..e55c72ba4970 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -32,9 +32,9 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('TextDisplay',) @@ -55,13 +55,16 @@ class TextDisplay(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ - def __init__(self, content: str, *, row: Optional[int] = None) -> None: + def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[str] = None) -> None: super().__init__() self.content: str = content self.row = row + self.id = id def to_component_dict(self): return { diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index cf9bfd3cc3d0..0e7def382559 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -32,10 +32,10 @@ if TYPE_CHECKING: from typing_extensions import Self - from .view import View + from .view import LayoutView from ..components import ThumbnailComponent -V = TypeVar('V', bound='View', covariant=True) +V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('Thumbnail',) @@ -62,6 +62,8 @@ class Thumbnail(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) + id: Optional[:class:`str`] + The ID of this component. This must be unique across the view. """ def __init__( @@ -71,6 +73,7 @@ def __init__( description: Optional[str] = None, spoiler: bool = False, row: Optional[int] = None, + id: Optional[str] = None, ) -> None: super().__init__() @@ -79,6 +82,7 @@ def __init__( self.spoiler: bool = spoiler self.row = row + self.id = id @property def width(self): diff --git a/discord/ui/view.py b/discord/ui/view.py index 9ea612aebe64..c63ac00e7277 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -36,6 +36,7 @@ TYPE_CHECKING, Tuple, Type, + Union, ) from functools import partial from itertools import groupby @@ -46,7 +47,6 @@ import time import os from .item import Item, ItemCallbackType -from .action_row import ActionRow from .dynamic import DynamicItem from ..components import ( Component, @@ -61,10 +61,12 @@ SeparatorComponent, ThumbnailComponent, ) +from ..utils import get as _utils_get # fmt: off __all__ = ( 'View', + 'LayoutView', ) # fmt: on @@ -80,6 +82,8 @@ from ..state import ConnectionState from .modal import Modal + ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]] + _log = logging.getLogger(__name__) @@ -188,57 +192,18 @@ def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: return self.callback(self.view, interaction, self.item) -class View: # NOTE: maybe add a deprecation warning in favour of LayoutView? - """Represents a UI view. - - This object must be inherited to create a UI within Discord. - - .. versionadded:: 2.0 - - Parameters - ----------- - timeout: Optional[:class:`float`] - Timeout in seconds from last interaction with the UI before no longer accepting input. - If ``None`` then there is no timeout. - """ - - __discord_ui_view__: ClassVar[bool] = True +class BaseView: + __discord_ui_view__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False - __view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - children: Dict[str, ItemCallbackType[Any, Any]] = {} - for base in reversed(cls.__mro__): - for name, member in base.__dict__.items(): - if hasattr(member, '__discord_ui_model_type__'): - children[name] = member - - if len(children) > 25: - raise TypeError('View cannot have more than 25 children') - - cls.__view_children_items__ = list(children.values()) - - def _init_children(self) -> List[Item[Self]]: - children = [] - - for func in self.__view_children_items__: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) # type: ignore - item._view = self - setattr(self, func.__name__, item) - children.append(item) - return children + __view_children_items__: ClassVar[List[ItemLike]] = [] - def __init__(self, *, timeout: Optional[float] = 180.0): + def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout = timeout self._children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self._children) self.id: str = os.urandom(16).hex() self._cache_key: Optional[int] = None - self.__cancel_callback: Optional[Callable[[View], None]] = None + self.__cancel_callback: Optional[Callable[[BaseView], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() @@ -246,12 +211,32 @@ def __init__(self, *, timeout: Optional[float] = 180.0): def _is_v2(self) -> bool: return False - @property - def width(self): - return 5 - def __repr__(self) -> str: - return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' + return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}' + + def _init_children(self) -> List[Item[Self]]: + children = [] + + for raw in self.__view_children_items__: + if isinstance(raw, Item): + raw._view = self + parent = getattr(raw, '__discord_ui_parent__', None) + if parent and parent._view is None: + parent._view = self + item = raw + else: + item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) + item.callback = _ViewCallback(raw, self, item) # type: ignore + item._view = self + setattr(self, raw.__name__, item) + parent = getattr(raw, '__discord_ui_parent__', None) + if parent: + if not self._is_v2(): + raise RuntimeError('This view cannot have v2 items') + parent._children.append(item) + children.append(item) + + return children async def __timeout_task_impl(self) -> None: while True: @@ -279,24 +264,7 @@ def has_components_v2(self) -> bool: return any(c._is_v2() for c in self.children) def to_components(self) -> List[Dict[str, Any]]: - def key(item: Item) -> int: - return item._rendered_row or 0 - - children = sorted(self._children, key=key) - components: List[Dict[str, Any]] = [] - for _, group in groupby(children, key=key): - children = [item.to_component_dict() for item in group] - if not children: - continue - - components.append( - { - 'type': 1, - 'components': children, - } - ) - - return components + return NotImplemented def _refresh_timeout(self) -> None: if self.__timeout: @@ -327,7 +295,7 @@ def children(self) -> List[Item[Self]]: return self._children.copy() @classmethod - def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Any: """Converts a message's components into a :class:`View`. The :attr:`.Message.components` of a message are read-only @@ -341,28 +309,8 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) The message with components to convert into a view. timeout: Optional[:class:`float`] The timeout of the converted view. - - Returns - -------- - :class:`View` - The converted view. This always returns a :class:`View` and not - one of its subclasses. """ - view = View(timeout=timeout) - row = 0 - for component in message.components: - if isinstance(component, ActionRowComponent): - for child in component.children: - item = _component_to_item(child) - item.row = row - view.add_item(item) - row += 1 - else: - item = _component_to_item(component) - item.row = row - view.add_item(item) - - return view + pass def add_item(self, item: Item[Any]) -> Self: """Adds an item to the view. @@ -385,18 +333,10 @@ def add_item(self, item: Item[Any]) -> Self: you tried to add is not allowed in this View. """ - if len(self._children) >= 25: - raise ValueError('maximum number of children exceeded') - if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') - if item._is_v2() and not self._is_v2(): - raise ValueError( - 'The item can only be added on LayoutView' - ) - - self.__weights.add_item(item) + raise ValueError('v2 items cannot be added to this view') item._view = self self._children.append(item) @@ -418,8 +358,6 @@ def remove_item(self, item: Item[Any]) -> Self: self._children.remove(item) except ValueError: pass - else: - self.__weights.remove_item(item) return self def clear_items(self) -> Self: @@ -429,9 +367,30 @@ def clear_items(self) -> Self: chaining. """ self._children.clear() - self.__weights.clear() return self + def get_item_by_id(self, id: str, /) -> Optional[Item[Self]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + .. versionadded:: 2.6 + + Parameters + ---------- + id: :class:`str` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + async def interaction_check(self, interaction: Interaction, /) -> bool: """|coro| @@ -599,61 +558,167 @@ async def wait(self) -> bool: return await self.__stopped -class LayoutView(View): - __view_children_items__: ClassVar[List[Item[Any]]] = [] - __view_pending_children__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] +class View(BaseView): # NOTE: maybe add a deprecation warning in favour of LayoutView? + """Represents a UI view. + + This object must be inherited to create a UI within Discord. + + .. versionadded:: 2.0 + + Parameters + ----------- + timeout: Optional[:class:`float`] + Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. + """ + + __discord_ui_view__: ClassVar[bool] = True + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, ItemCallbackType[Any, Any]] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if hasattr(member, '__discord_ui_model_type__'): + children[name] = member + + if len(children) > 25: + raise TypeError('View cannot have more than 25 children') + + cls.__view_children_items__ = list(children.values()) + + def __init__(self, *, timeout: Optional[float] = 180.0): + super().__init__(timeout=timeout) + self.__weights = _ViewWeights(self._children) + + @property + def width(self): + return 5 + + def to_components(self) -> List[Dict[str, Any]]: + def key(item: Item) -> int: + return item._rendered_row or 0 + + children = sorted(self._children, key=key) + components: List[Dict[str, Any]] = [] + for _, group in groupby(children, key=key): + children = [item.to_component_dict() for item in group] + if not children: + continue + + components.append( + { + 'type': 1, + 'components': children, + } + ) + + return components + + @classmethod + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: + """Converts a message's components into a :class:`View`. + + The :attr:`.Message.components` of a message are read-only + and separate types from those in the ``discord.ui`` namespace. + In order to modify and edit message components they must be + converted into a :class:`View` first. + + Parameters + ----------- + message: :class:`discord.Message` + The message with components to convert into a view. + timeout: Optional[:class:`float`] + The timeout of the converted view. + + Returns + -------- + :class:`View` + The converted view. This always returns a :class:`View` and not + one of its subclasses. + """ + view = View(timeout=timeout) + row = 0 + for component in message.components: + if isinstance(component, ActionRowComponent): + for child in component.children: + item = _component_to_item(child) + item.row = row + if item._is_v2(): + raise RuntimeError('v2 components cannot be added to this View') + view.add_item(item) + row += 1 + else: + item = _component_to_item(component) + item.row = row + if item._is_v2(): + raise RuntimeError('v2 components cannot be added to this View') + view.add_item(item) + + return view - def __init__(self, *, timeout: Optional[float] = 180) -> None: + def add_item(self, item: Item[Any]) -> Self: + if len(self._children) >= 25: + raise ValueError('maximum number of children exceeded') + + super().add_item(item) + try: + self.__weights.add_item(item) + except ValueError as e: + # if the item has no space left then remove it from _children + self._children.remove(item) + raise e + + return self + + def remove_item(self, item: Item[Any]) -> Self: + try: + self._children.remove(item) + except ValueError: + pass + else: + self.__weights.remove_item(item) + return self + + def clear_items(self) -> Self: + super().clear_items() + self.__weights.clear() + return self + + +class LayoutView(BaseView): + """Represents a layout view for components v2. + + Unline :class:`View` this allows for components v2 to exist + within it. + + .. versionadded:: 2.6 + + Parameters + ---------- + timeout: Optional[:class:`float`] + Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. + """ + + def __init__(self, *, timeout: Optional[float] = 180.0) -> None: super().__init__(timeout=timeout) - self.__weights.weights.extend([0, 0, 0, 0, 0]) def __init_subclass__(cls) -> None: children: Dict[str, Item[Any]] = {} - pending: Dict[str, ItemCallbackType[Any, Any]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if isinstance(member, Item): children[name] = member elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): - pending[name] = member + children[name] = member if len(children) > 10: raise TypeError('LayoutView cannot have more than 10 top-level children') cls.__view_children_items__ = list(children.values()) - cls.__view_pending_children__ = list(pending.values()) - - def _init_children(self) -> List[Item[Self]]: - children = [] - - for func in self.__view_pending_children__: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) - item.callback = _ViewCallback(func, self, item) - item._view = self - setattr(self, func.__name__, item) - parent: ActionRow = func.__discord_ui_parent__ - parent.add_item(item) - - for i in self.__view_children_items__: - if isinstance(i, Item): - if getattr(i, '_parent', None): - # this is for ActionRows which have decorators such as - # @action_row.button and @action_row.select that will convert - # those callbacks into their types but will have a _parent - # attribute which is checked here so the item is not added twice - continue - i._view = self - if getattr(i, '__discord_ui_action_row__', False): - i._update_children_view(self) # type: ignore - children.append(i) - else: - # guard just in case - raise TypeError( - 'LayoutView can only have items' - ) - - return children def _is_v2(self) -> bool: return True @@ -670,11 +735,49 @@ def to_components(self): return child + def add_item(self, item: Item[Any]) -> Self: + if len(self._children) >= 10: + raise ValueError('maximum number of children exceeded') + super().add_item(item) + return self + + @classmethod + def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView: + """Converts a message's components into a :class:`LayoutView`. + + The :attr:`.Message.components` of a message are read-only + and separate types from those in the ``discord.ui`` namespace. + In order to modify and edit message components they must be + converted into a :class:`LayoutView` first. + + Unlike :meth:`View.from_message` this works for + + Parameters + ----------- + message: :class:`discord.Message` + The message with components to convert into a view. + timeout: Optional[:class:`float`] + The timeout of the converted view. + + Returns + -------- + :class:`LayoutView` + The converted view. This always returns a :class:`LayoutView` and not + one of its subclasses. + """ + view = LayoutView(timeout=timeout) + for component in message.components: + item = _component_to_item(component) + item.row = 0 + view.add_item(item) + + return view + class ViewStore: def __init__(self, state: ConnectionState): # entity_id: {(component_type, custom_id): Item} - self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {} + self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {} # message_id: View self._synced_message_views: Dict[int, View] = {} # custom_id: Modal @@ -684,7 +787,7 @@ def __init__(self, state: ConnectionState): self._state: ConnectionState = state @property - def persistent_views(self) -> Sequence[View]: + def persistent_views(self) -> Sequence[BaseView]: # fmt: off views = { item.view.id: item.view @@ -722,7 +825,7 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: is_fully_dynamic = item._update_store_data( # type: ignore dispatch_info, self._dynamic_items, - ) + ) or is_fully_dynamic elif getattr(item, '__discord_ui_action_row__', False): is_fully_dynamic = item._update_store_data( # type: ignore dispatch_info, @@ -784,7 +887,7 @@ async def schedule_dynamic_item_call( return # Swap the item in the view with our new dynamic item - view._children[base_item_index] = item + view._children[base_item_index] = item # type: ignore item._view = view item._rendered_row = base_item._rendered_row item._refresh_state(interaction, interaction.data) # type: ignore @@ -826,7 +929,7 @@ def dispatch_view(self, component_type: int, custom_id: str, interaction: Intera key = (component_type, custom_id) # The entity_id can either be message_id, interaction_id, or None in that priority order. - item: Optional[Item[View]] = None + item: Optional[Item[BaseView]] = None if message_id is not None: item = self._views.get(message_id, {}).get(key) @@ -878,7 +981,7 @@ def remove_interaction_mapping(self, interaction_id: int) -> None: def is_message_tracked(self, message_id: int) -> bool: return message_id in self._synced_message_views - def remove_message_tracking(self, message_id: int) -> Optional[View]: + def remove_message_tracking(self, message_id: int) -> Optional[BaseView]: return self._synced_message_views.pop(message_id, None) def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None: From 6c02a7dd9a28d01acfea3e07c9c261af28dfcf9c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:33:05 +0100 Subject: [PATCH 251/354] chore: docs --- docs/interactions/api.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index df2d7418dfff..c62e50a3a748 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -604,6 +604,15 @@ Modal :members: :inherited-members: +LayoutView +~~~~~~~~~~ + +.. attributetable:: discord.ui.LayoutView + +.. autoclass:: discord.ui.LayoutView + :member: + :inherited-members: + Item ~~~~~~~ From 67bfa57f32572cf90906c418d96a414d2f6db5ce Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:35:01 +0100 Subject: [PATCH 252/354] chore: run black --- discord/ui/action_row.py | 4 +--- discord/ui/view.py | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 1df526cba41c..5727860562ba 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -331,7 +331,6 @@ def select( ) -> SelectCallbackDecorator[V, UserSelectT]: ... - @overload def select( self, @@ -348,7 +347,6 @@ def select( ) -> SelectCallbackDecorator[V, RoleSelectT]: ... - @overload def select( self, @@ -365,7 +363,6 @@ def select( ) -> SelectCallbackDecorator[V, ChannelSelectT]: ... - @overload def select( self, @@ -484,6 +481,7 @@ def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, Bas @classmethod def from_component(cls, component: ActionRowComponent) -> ActionRow: from .view import _component_to_item + self = cls() for cmp in component.children: self.add_item(_component_to_item(cmp)) diff --git a/discord/ui/view.py b/discord/ui/view.py index c63ac00e7277..cd8e50f375a1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -750,7 +750,7 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) In order to modify and edit message components they must be converted into a :class:`LayoutView` first. - Unlike :meth:`View.from_message` this works for + Unlike :meth:`View.from_message` this works for Parameters ----------- @@ -822,15 +822,21 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: self._dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): if getattr(item, '__discord_ui_container__', False): - is_fully_dynamic = item._update_store_data( # type: ignore - dispatch_info, - self._dynamic_items, - ) or is_fully_dynamic + is_fully_dynamic = ( + item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) + or is_fully_dynamic + ) elif getattr(item, '__discord_ui_action_row__', False): - is_fully_dynamic = item._update_store_data( # type: ignore - dispatch_info, - self._dynamic_items, - ) or is_fully_dynamic + is_fully_dynamic = ( + item._update_store_data( # type: ignore + dispatch_info, + self._dynamic_items, + ) + or is_fully_dynamic + ) else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 0b23f1022850c6fb538669320667bc201f14d066 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:36:05 +0100 Subject: [PATCH 253/354] chore: fix discord.ui.View --- docs/interactions/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index c62e50a3a748..46a2fa1888ee 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -594,6 +594,7 @@ View .. autoclass:: discord.ui.View :members: + :inherited-members: Modal ~~~~~~ From eae08956dec704da485e905a1ce816ec01e4f445 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:38:33 +0100 Subject: [PATCH 254/354] chore: fix linting --- discord/client.py | 8 ++++---- discord/state.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/client.py b/discord/client.py index b997bd96f4af..2eaae2455b76 100644 --- a/discord/client.py +++ b/discord/client.py @@ -72,7 +72,7 @@ from .backoff import ExponentialBackoff from .webhook import Webhook from .appinfo import AppInfo -from .ui.view import View +from .ui.view import BaseView from .ui.dynamic import DynamicItem from .stage_instance import StageInstance from .threads import Thread @@ -3149,7 +3149,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: self._connection.remove_dynamic_items(*items) - def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: + def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None: """Registers a :class:`~discord.ui.View` for persistent listening. This method should be used for when a view is comprised of components @@ -3175,7 +3175,7 @@ def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: and all their components have an explicitly provided custom_id. """ - if not isinstance(view, View): + if not isinstance(view, BaseView): raise TypeError(f'expected an instance of View not {view.__class__.__name__}') if not view.is_persistent(): @@ -3187,7 +3187,7 @@ def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: self._connection.store_view(view, message_id) @property - def persistent_views(self) -> Sequence[View]: + def persistent_views(self) -> Sequence[BaseView]: """Sequence[:class:`.View`]: A sequence of persistent views added to the client. .. versionadded:: 2.0 diff --git a/discord/state.py b/discord/state.py index c4b71b368ad3..dd8c0d561dc5 100644 --- a/discord/state.py +++ b/discord/state.py @@ -71,7 +71,7 @@ from .invite import Invite from .integrations import _integration_factory from .interactions import Interaction -from .ui.view import ViewStore, View +from .ui.view import ViewStore, BaseView from .scheduled_event import ScheduledEvent from .stage_instance import StageInstance from .threads import Thread, ThreadMember @@ -412,12 +412,12 @@ def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data) return sticker - def store_view(self, view: View, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None: + def store_view(self, view: BaseView, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None: if interaction_id is not None: self._view_store.remove_interaction_mapping(interaction_id) self._view_store.add_view(view, message_id) - def prevent_view_updates_for(self, message_id: int) -> Optional[View]: + def prevent_view_updates_for(self, message_id: int) -> Optional[BaseView]: return self._view_store.remove_message_tracking(message_id) def store_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: @@ -427,7 +427,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: self._view_store.remove_dynamic_items(*items) @property - def persistent_views(self) -> Sequence[View]: + def persistent_views(self) -> Sequence[BaseView]: return self._view_store.persistent_views @property From c63ad950ee868f03d502da7a3c61ade2070bf223 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:41:30 +0100 Subject: [PATCH 255/354] chore: more linting things and docs --- discord/ui/view.py | 4 ++-- docs/interactions/api.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index cd8e50f375a1..22dad5e98ac4 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -461,7 +461,7 @@ async def _scheduled_task(self, item: Item, interaction: Interaction): return await self.on_error(interaction, e, item) def _start_listening_from_store(self, store: ViewStore) -> None: - self.__cancel_callback = partial(store.remove_view) + self.__cancel_callback = partial(store.remove_view) # type: ignore if self.timeout: if self.__timeout_task is not None: self.__timeout_task.cancel() @@ -808,7 +808,7 @@ def remove_dynamic_items(self, *items: Type[DynamicItem[Item[Any]]]) -> None: pattern = item.__discord_ui_compiled_template__ self._dynamic_items.pop(pattern, None) - def add_view(self, view: View, message_id: Optional[int] = None) -> None: + def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: view._start_listening_from_store(self) if view.__discord_ui_modal__: self._modals[view.custom_id] = view # type: ignore diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 46a2fa1888ee..a4005882341b 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -611,7 +611,7 @@ LayoutView .. attributetable:: discord.ui.LayoutView .. autoclass:: discord.ui.LayoutView - :member: + :members: :inherited-members: Item From 7338da2b11f0cd7b108d7e8164d838c1d9fa7c79 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:43:06 +0100 Subject: [PATCH 256/354] fix linting yet again --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 22dad5e98ac4..d0f187187f0b 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -779,7 +779,7 @@ def __init__(self, state: ConnectionState): # entity_id: {(component_type, custom_id): Item} self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {} # message_id: View - self._synced_message_views: Dict[int, View] = {} + self._synced_message_views: Dict[int, BaseView] = {} # custom_id: Modal self._modals: Dict[str, Modal] = {} # component_type is the key From c5ffc6a079826ac6737712040bffdae4ab5d3321 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:51:51 +0100 Subject: [PATCH 257/354] chore: fix LayoutView.to_components --- discord/ext/commands/context.py | 20 ++++++++++---------- discord/ui/view.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 93303973523a..0e81f33b897f 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -48,7 +48,7 @@ from discord.mentions import AllowedMentions from discord.sticker import GuildSticker, StickerItem from discord.message import MessageReference, PartialMessage - from discord.ui import View + from discord.ui.view import BaseView from discord.types.interactions import ApplicationCommandInteractionData from discord.poll import Poll @@ -642,7 +642,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -664,7 +664,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -686,7 +686,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -708,7 +708,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -831,7 +831,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -853,7 +853,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -875,7 +875,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -897,7 +897,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -920,7 +920,7 @@ async def send( allowed_mentions: Optional[AllowedMentions] = None, reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, - view: Optional[View] = None, + view: Optional[BaseView] = None, suppress_embeds: bool = False, ephemeral: bool = False, silent: bool = False, diff --git a/discord/ui/view.py b/discord/ui/view.py index d0f187187f0b..aff9310091fb 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -733,7 +733,7 @@ def to_components(self): child.to_component_dict(), ) - return child + return components def add_item(self, item: Item[Any]) -> Self: if len(self._children) >= 10: From 59991e9ed7ce411dc87470b9695434030e4b65d3 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:06:37 +0100 Subject: [PATCH 258/354] chore: fix Container.to_components returning NotImplemented --- discord/ui/container.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 1b50eceb9c32..da1770028322 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -26,7 +26,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar from .item import Item -from .view import View, _component_to_item, LayoutView +from .view import BaseView, _component_to_item, LayoutView from .dynamic import DynamicItem from ..enums import ComponentType from ..utils import MISSING @@ -42,7 +42,7 @@ __all__ = ('Container',) -class Container(View, Item[V]): +class Container(BaseView, Item[V]): """Represents a UI container. .. versionadded:: 2.6 @@ -59,9 +59,6 @@ class Container(View, Item[V]): spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults to ``False``. - timeout: Optional[:class:`float`] - Timeout in seconds from last interaction with the UI before no longer accepting input. - If ``None`` then there is no timeout. row: Optional[:class:`int`] The relative row this container belongs to. By default items are arranged automatically into those rows. If you'd @@ -73,8 +70,6 @@ class Container(View, Item[V]): The ID of this component. This must be unique across the view. """ - __discord_ui_container__ = True - def __init__( self, children: List[Item[Any]] = MISSING, @@ -82,11 +77,10 @@ def __init__( accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, - timeout: Optional[float] = 180, row: Optional[int] = None, id: Optional[str] = None, ) -> None: - super().__init__(timeout=timeout) + super().__init__(timeout=None) if children is not MISSING: if len(children) + len(self._children) > 10: raise ValueError('maximum number of components exceeded') @@ -134,8 +128,14 @@ def _is_v2(self) -> bool: def is_dispatchable(self) -> bool: return any(c.is_dispatchable() for c in self.children) + def to_components(self) -> List[Dict[str, Any]]: + components = [] + for child in self._children: + components.append(child.to_component_dict()) + return components + def to_component_dict(self) -> Dict[str, Any]: - components = super().to_components() + components = self.to_components() return { 'type': self.type.value, 'accent_color': self._colour.value if self._colour else None, From 502051af7132b55d67504f140b7e25e53afa91c5 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:27:19 +0100 Subject: [PATCH 259/354] chore: update ActionRow and View --- discord/ui/__init__.py | 1 + discord/ui/action_row.py | 28 +++++++++++----------------- discord/ui/view.py | 7 +++---- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index 62a78634c72d..4d613f14faf0 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -23,3 +23,4 @@ from .separator import * from .text_display import * from .thumbnail import * +from .action_row import * diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 5727860562ba..a7017159a177 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -43,7 +43,7 @@ ) from .item import Item, ItemCallbackType -from .button import Button +from .button import Button, button as _button from .dynamic import DynamicItem from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect from ..components import ActionRow as ActionRowComponent @@ -281,22 +281,16 @@ def button( """ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: - if not inspect.iscoroutinefunction(func): - raise TypeError('button function must be a coroutine function') - - func.__discord_ui_parent__ = self - func.__discord_ui_modal_type__ = Button - func.__discord_ui_model_kwargs__ = { - 'style': style, - 'custom_id': custom_id, - 'url': None, - 'disabled': disabled, - 'label': label, - 'emoji': emoji, - 'row': None, - 'sku_id': None, - } - return func + ret = _button( + label=label, + custom_id=custom_id, + disabled=disabled, + style=style, + emoji=emoji, + row=None, + )(func) + ret.__discord_ui_parent__ = self # type: ignore + return ret # type: ignore return decorator # type: ignore diff --git a/discord/ui/view.py b/discord/ui/view.py index aff9310091fb..bafcfedff4d0 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -223,7 +223,7 @@ def _init_children(self) -> List[Item[Self]]: parent = getattr(raw, '__discord_ui_parent__', None) if parent and parent._view is None: parent._view = self - item = raw + children.append(raw) else: item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) item.callback = _ViewCallback(raw, self, item) # type: ignore @@ -231,10 +231,9 @@ def _init_children(self) -> List[Item[Self]]: setattr(self, raw.__name__, item) parent = getattr(raw, '__discord_ui_parent__', None) if parent: - if not self._is_v2(): - raise RuntimeError('This view cannot have v2 items') parent._children.append(item) - children.append(item) + continue + children.append(item) return children From f1f6ef82ab440931abf8be49e34f735149f7be0d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:29:19 +0100 Subject: [PATCH 260/354] chore: remove unused imports --- discord/ui/action_row.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index a7017159a177..4daf0283973d 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -23,7 +23,6 @@ """ from __future__ import annotations -import inspect import os from typing import ( TYPE_CHECKING, From 9e18c5af8142ead1030a3df2f7bb587043d3055d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:42:24 +0100 Subject: [PATCH 261/354] chore: typing stuff --- discord/abc.py | 10 +++++----- discord/channel.py | 4 ++-- discord/http.py | 4 ++-- discord/interactions.py | 10 +++++----- discord/message.py | 14 +++++++------- discord/webhook/async_.py | 8 ++++---- discord/webhook/sync.py | 6 ++++-- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 70531fb2005e..666120c543e3 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -95,7 +95,7 @@ ) from .poll import Poll from .threads import Thread - from .ui.view import View + from .ui.view import BaseView from .types.channel import ( PermissionOverwrite as PermissionOverwritePayload, Channel as ChannelPayload, @@ -1388,7 +1388,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1409,7 +1409,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1430,7 +1430,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1451,7 +1451,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., diff --git a/discord/channel.py b/discord/channel.py index a306707d6fdb..3dc43d388549 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -100,7 +100,7 @@ from .file import File from .user import ClientUser, User, BaseUser from .guild import Guild, GuildChannel as GuildChannelType - from .ui.view import View + from .ui.view import BaseView from .types.channel import ( TextChannel as TextChannelPayload, NewsChannel as NewsChannelPayload, @@ -2857,7 +2857,7 @@ async def create_thread( allowed_mentions: AllowedMentions = MISSING, mention_author: bool = MISSING, applied_tags: Sequence[ForumTag] = MISSING, - view: View = MISSING, + view: BaseView = MISSING, suppress_embeds: bool = False, reason: Optional[str] = None, ) -> ThreadWithMessage: diff --git a/discord/http.py b/discord/http.py index c6e4d1377277..e0fca595861d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -64,7 +64,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from .ui.view import View + from .ui.view import BaseView from .embeds import Embed from .message import Attachment from .poll import Poll @@ -150,7 +150,7 @@ def handle_message_parameters( embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, message_reference: Optional[message.MessageReference] = MISSING, stickers: Optional[SnowflakeList] = MISSING, diff --git a/discord/interactions.py b/discord/interactions.py index b9d9a4d11ea7..ddc5094a4d2d 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -76,7 +76,7 @@ from .mentions import AllowedMentions from aiohttp import ClientSession from .embeds import Embed - from .ui.view import View + from .ui.view import BaseView from .app_commands.models import Choice, ChoiceT from .ui.modal import Modal from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel @@ -476,7 +476,7 @@ async def edit_original_response( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, poll: Poll = MISSING, ) -> InteractionMessage: @@ -897,7 +897,7 @@ async def send_message( embeds: Sequence[Embed] = MISSING, file: File = MISSING, files: Sequence[File] = MISSING, - view: View = MISSING, + view: BaseView = MISSING, tts: bool = False, ephemeral: bool = False, allowed_mentions: AllowedMentions = MISSING, @@ -1046,7 +1046,7 @@ async def edit_message( embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, @@ -1334,7 +1334,7 @@ async def edit( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, delete_after: Optional[float] = None, poll: Poll = MISSING, diff --git a/discord/message.py b/discord/message.py index c0a853ce3cba..0dee8df9c426 100644 --- a/discord/message.py +++ b/discord/message.py @@ -101,7 +101,7 @@ from .mentions import AllowedMentions from .user import User from .role import Role - from .ui.view import View + from .ui.view import BaseView EmojiInputType = Union[Emoji, PartialEmoji, str] @@ -1305,7 +1305,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -1318,7 +1318,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -1331,7 +1331,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = MISSING, delete_after: Optional[float] = None, allowed_mentions: Optional[AllowedMentions] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, ) -> Message: """|coro| @@ -2839,7 +2839,7 @@ async def edit( suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -2853,7 +2853,7 @@ async def edit( suppress: bool = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[View] = ..., + view: Optional[BaseView] = ..., ) -> Message: ... @@ -2867,7 +2867,7 @@ async def edit( suppress: bool = False, delete_after: Optional[float] = None, allowed_mentions: Optional[AllowedMentions] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, ) -> Message: """|coro| diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index f1cfb573bb71..2ddc451f6d04 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -71,7 +71,7 @@ from ..emoji import Emoji from ..channel import VoiceChannel from ..abc import Snowflake - from ..ui.view import View + from ..ui.view import BaseView from ..poll import Poll import datetime from ..types.webhook import ( @@ -1619,7 +1619,7 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: View = MISSING, + view: BaseView = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: Literal[True], @@ -1644,7 +1644,7 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: View = MISSING, + view: BaseView = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: Literal[False] = ..., @@ -1668,7 +1668,7 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: View = MISSING, + view: BaseView = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: bool = False, diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 171931b12ea2..db59b4659866 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -66,7 +66,7 @@ from ..message import Attachment from ..abc import Snowflake from ..state import ConnectionState - from ..ui import View + from ..ui.view import BaseView from ..types.webhook import ( Webhook as WebhookPayload, ) @@ -876,6 +876,7 @@ def send( silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, + view: BaseView = MISSING, ) -> SyncWebhookMessage: ... @@ -899,6 +900,7 @@ def send( silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, + view: BaseView = MISSING, ) -> None: ... @@ -921,7 +923,7 @@ def send( silent: bool = False, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, - view: View = MISSING, + view: BaseView = MISSING, ) -> Optional[SyncWebhookMessage]: """Sends a message using the webhook. From e660010c2501e0be7bd9cceef6b84264e1e81b9a Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:46:32 +0100 Subject: [PATCH 262/354] chore: more typing stuff --- discord/abc.py | 2 +- discord/message.py | 8 ++++---- discord/webhook/async_.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 666120c543e3..5d264283e69b 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1473,7 +1473,7 @@ async def send( allowed_mentions: Optional[AllowedMentions] = None, reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, mention_author: Optional[bool] = None, - view: Optional[View] = None, + view: Optional[BaseView] = None, suppress_embeds: bool = False, silent: bool = False, poll: Optional[Poll] = None, diff --git a/discord/message.py b/discord/message.py index 0dee8df9c426..b3b807610676 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1760,7 +1760,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1781,7 +1781,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1802,7 +1802,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1823,7 +1823,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: View = ..., + view: BaseView = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2ddc451f6d04..d62807779051 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -552,7 +552,7 @@ def interaction_message_response_params( embed: Optional[Embed] = MISSING, embeds: Sequence[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = MISSING, previous_allowed_mentions: Optional[AllowedMentions] = None, poll: Poll = MISSING, @@ -809,7 +809,7 @@ async def edit( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, ) -> WebhookMessage: """|coro| @@ -1946,7 +1946,7 @@ async def edit_message( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, thread: Snowflake = MISSING, ) -> WebhookMessage: From c48c512d889139eeda732ce4fc146bd50f39583e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 22:07:39 +0100 Subject: [PATCH 263/354] chore: some fixes of bugs reported on the bikeshedding post --- discord/ui/action_row.py | 11 ++--- discord/ui/button.py | 7 ++- discord/ui/container.py | 90 ++++++++++++++++++++++++++++++------- discord/ui/file.py | 4 +- discord/ui/item.py | 9 ++-- discord/ui/media_gallery.py | 4 +- discord/ui/section.py | 4 +- discord/ui/select.py | 44 +++++++++++++++++- discord/ui/separator.py | 4 +- discord/ui/text_display.py | 4 +- discord/ui/thumbnail.py | 8 ++-- discord/ui/view.py | 8 ++++ 12 files changed, 155 insertions(+), 42 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 4daf0283973d..510d6175bf1a 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -94,19 +94,20 @@ class ActionRow(Item[V]): Parameters ---------- - id: Optional[:class:`str`] - The ID of this action row. Defaults to ``None``. + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. """ __action_row_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] __discord_ui_action_row__: ClassVar[bool] = True + __pending_view__: ClassVar[bool] = True - def __init__(self, *, id: Optional[str] = None) -> None: + def __init__(self, *, id: Optional[int] = None) -> None: super().__init__() - - self.id: str = id or os.urandom(16).hex() self._children: List[Item[Any]] = self._init_children() + self.id = id + def __init_subclass__(cls) -> None: super().__init_subclass__() diff --git a/discord/ui/button.py b/discord/ui/button.py index df21c770fc4b..82a485f9110f 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -83,6 +83,10 @@ class Button(Item[V]): nor ``custom_id``. .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + + .. versionadded:: 2.6 """ __item_repr_attributes__: Tuple[str, ...] = ( @@ -106,6 +110,7 @@ def __init__( emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, sku_id: Optional[int] = None, + id: Optional[int] = None, ): super().__init__() if custom_id is not None and (url is not None or sku_id is not None): @@ -147,7 +152,7 @@ def __init__( ) self._parent: Optional[ActionRow] = None self.row = row - self.id = custom_id + self.id = id @property def style(self) -> ButtonStyle: diff --git a/discord/ui/container.py b/discord/ui/container.py index da1770028322..b60c1ec407fc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,10 +23,10 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union -from .item import Item -from .view import BaseView, _component_to_item, LayoutView +from .item import Item, ItemCallbackType +from .view import _component_to_item, LayoutView from .dynamic import DynamicItem from ..enums import ComponentType from ..utils import MISSING @@ -36,13 +36,26 @@ from ..colour import Colour, Color from ..components import Container as ContainerComponent + from ..interactions import Interaction V = TypeVar('V', bound='LayoutView', covariant=True) __all__ = ('Container',) -class Container(BaseView, Item[V]): +class _ContainerCallback: + __slots__ = ('container', 'callback', 'item') + + def __init__(self, callback: ItemCallbackType[Any, Any], container: Container, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any, Any] = callback + self.container: Container = container + self.item: Item[Any] = item + + def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: + return self.callback(self.container, interaction, self.item) + + +class Container(Item[V]): """Represents a UI container. .. versionadded:: 2.6 @@ -66,41 +79,86 @@ class Container(BaseView, Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ + __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] + __pending_view__: ClassVar[bool] = True + def __init__( self, - children: List[Item[Any]] = MISSING, + children: List[Item[V]] = MISSING, *, accent_colour: Optional[Colour] = None, accent_color: Optional[Color] = None, spoiler: bool = False, row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: - super().__init__(timeout=None) + self._children: List[Item[V]] = self._init_children() + if children is not MISSING: if len(children) + len(self._children) > 10: - raise ValueError('maximum number of components exceeded') - self._children.extend(children) + raise ValueError('maximum number of children exceeded') self.spoiler: bool = spoiler self._colour = accent_colour or accent_color self._view: Optional[V] = None - self._row: Optional[int] = None - self._rendered_row: Optional[int] = None - self.row: Optional[int] = row - self.id: Optional[str] = id + self.row = row + self.id = id + + def _init_children(self) -> List[Item[Any]]: + children = [] + + for raw in self.__container_children_items__: + if isinstance(raw, Item): + children.append(raw) + else: + # action rows can be created inside containers, and then callbacks can exist here + # so we create items based off them + item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) + item.callback = _ContainerCallback(raw, self, item) # type: ignore + setattr(self, raw.__name__, item) + # this should not fail because in order for a function to be here it should be from + # an action row and must have passed the check in __init_subclass__, but still + # guarding it + parent = getattr(raw, '__discord_ui_parent__', None) + if parent is None: + raise RuntimeError(f'{raw.__name__} is not a valid item for a Container') + parent._children.append(item) + # we donnot append it to the children list because technically these buttons and + # selects are not from the container but the action row itself. + + return children + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + children: Dict[str, Union[ItemCallbackType[Any, Any], Item[Any]]] = {} + for base in reversed(cls.__mro__): + for name, member in base.__dict__.items(): + if isinstance(member, Item): + children[name] = member + if hasattr(member, '__discord_ui_model_type__') and hasattr(member, '__discord_ui_parent__'): + children[name] = member + + cls.__container_children_items__ = list(children.values()) + + def _update_children_view(self, view) -> None: + for child in self._children: + child._view = view + if getattr(child, '__pending_view__', False): + # if the item is an action row which child's view can be updated, then update it + child._update_children_view(view) # type: ignore @property - def children(self) -> List[Item[Self]]: + def children(self) -> List[Item[V]]: """List[:class:`Item`]: The children of this container.""" return self._children.copy() @children.setter - def children(self, value: List[Item[Any]]) -> None: + def children(self, value: List[Item[V]]) -> None: self._children = value @property diff --git a/discord/ui/file.py b/discord/ui/file.py index 2654d351c305..7d065f0ffcbb 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -59,7 +59,7 @@ class File(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -69,7 +69,7 @@ def __init__( *, spoiler: bool = False, row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: super().__init__() self._underlying = FileComponent._raw_construct( diff --git a/discord/ui/item.py b/discord/ui/item.py index 1fa68b68c701..bcee854a87aa 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -70,7 +70,7 @@ def __init__(self): # actually affect the intended purpose of this check because from_component is # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False - self._id: Optional[str] = None + self._id: Optional[int] = None self._max_row: int = 5 if not self._is_v2() else 10 def to_component_dict(self) -> Dict[str, Any]: @@ -126,14 +126,13 @@ def view(self) -> Optional[V]: return self._view @property - def id(self) -> Optional[str]: - """Optional[:class:`str`]: The ID of this component. For non v2 components this is the - equivalent to ``custom_id``. + def id(self) -> Optional[int]: + """Optional[:class:`int`]: The ID of this component. """ return self._id @id.setter - def id(self, value: Optional[str]) -> None: + def id(self, value: Optional[int]) -> None: self._id = value async def callback(self, interaction: Interaction[ClientT]) -> Any: diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index f9e1fb2644c7..ee0fb3cf0cb0 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -60,7 +60,7 @@ class MediaGallery(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -69,7 +69,7 @@ def __init__( items: List[MediaGalleryItem], *, row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: super().__init__() diff --git a/discord/ui/section.py b/discord/ui/section.py index ba919beb81b6..0aa164d883af 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -59,7 +59,7 @@ class Section(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -74,7 +74,7 @@ def __init__( *, accessory: Item[Any], row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: super().__init__() self._children: List[Item[Any]] = [] diff --git a/discord/ui/select.py b/discord/ui/select.py index f5a9fcbee2e1..efa8a9e6880e 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -239,6 +239,7 @@ def __init__( options: List[SelectOption] = MISSING, channel_types: List[ChannelType] = MISSING, default_values: Sequence[SelectDefaultValue] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__() self._provided_custom_id = custom_id is not MISSING @@ -259,7 +260,7 @@ def __init__( ) self.row = row - self.id = custom_id if custom_id is not MISSING else None + self.id = id self._parent: Optional[ActionRow] = None self._values: List[PossibleValue] = [] @@ -393,6 +394,10 @@ class Select(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('options',) @@ -407,6 +412,7 @@ def __init__( options: List[SelectOption] = MISSING, disabled: bool = False, row: Optional[int] = None, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -417,6 +423,7 @@ def __init__( disabled=disabled, options=options, row=row, + id=id, ) @property @@ -548,6 +555,10 @@ class UserSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -562,6 +573,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -572,6 +584,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -640,6 +653,10 @@ class RoleSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -654,6 +671,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -664,6 +682,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -728,6 +747,10 @@ class MentionableSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ('default_values',) @@ -742,6 +765,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -752,6 +776,7 @@ def __init__( disabled=disabled, row=row, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -822,6 +847,10 @@ class ChannelSelect(BaseSelect[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __component_attributes__ = BaseSelect.__component_attributes__ + ( @@ -840,6 +869,7 @@ def __init__( disabled: bool = False, row: Optional[int] = None, default_values: Sequence[ValidDefaultValues] = MISSING, + id: Optional[int] = None, ) -> None: super().__init__( self.type, @@ -851,6 +881,7 @@ def __init__( row=row, channel_types=channel_types, default_values=_handle_select_defaults(default_values, self.type), + id=id, ) @property @@ -902,6 +933,7 @@ def select( max_values: int = ..., disabled: bool = ..., row: Optional[int] = ..., + id: Optional[int] = ..., ) -> SelectCallbackDecorator[V, SelectT]: ... @@ -919,6 +951,7 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., + id: Optional[int] = ..., ) -> SelectCallbackDecorator[V, UserSelectT]: ... @@ -936,6 +969,7 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., + id: Optional[int] = ..., ) -> SelectCallbackDecorator[V, RoleSelectT]: ... @@ -953,6 +987,7 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., + id: Optional[int] = ..., ) -> SelectCallbackDecorator[V, ChannelSelectT]: ... @@ -970,6 +1005,7 @@ def select( disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., + id: Optional[int] = ..., ) -> SelectCallbackDecorator[V, MentionableSelectT]: ... @@ -986,6 +1022,7 @@ def select( disabled: bool = False, default_values: Sequence[ValidDefaultValues] = MISSING, row: Optional[int] = None, + id: Optional[int] = None, ) -> SelectCallbackDecorator[V, BaseSelectT]: """A decorator that attaches a select menu to a component. @@ -1065,6 +1102,10 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe Number of items must be in range of ``min_values`` and ``max_values``. .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: @@ -1083,6 +1124,7 @@ def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, Bas 'min_values': min_values, 'max_values': max_values, 'disabled': disabled, + 'id': id, } if issubclass(callback_cls, Select): func.__discord_ui_model_kwargs__['options'] = options diff --git a/discord/ui/separator.py b/discord/ui/separator.py index b9ff955adcc5..394e9ac78df5 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -58,7 +58,7 @@ class Separator(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -68,7 +68,7 @@ def __init__( visible: bool = True, spacing: SeparatorSize = SeparatorSize.small, row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: super().__init__() self._underlying = SeparatorComponent._raw_construct( diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index e55c72ba4970..8e22905ebc47 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -55,11 +55,11 @@ class TextDisplay(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ - def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[str] = None) -> None: + def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[int] = None) -> None: super().__init__() self.content: str = content diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 0e7def382559..e9a2c13f5a9b 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -48,8 +48,8 @@ class Thumbnail(Item[V]): Parameters ---------- media: Union[:class:`str`, :class:`discord.UnfurledMediaItem`] - The media of the thumbnail. This can be a string that points to a local - attachment uploaded within this item. URLs must match the ``attachment://file-name.extension`` + The media of the thumbnail. This can be a URL or a reference + to an attachment that matches the ``attachment://filename.extension`` structure. description: Optional[:class:`str`] The description of this thumbnail. Defaults to ``None``. @@ -62,7 +62,7 @@ class Thumbnail(Item[V]): passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 9 (i.e. zero indexed) - id: Optional[:class:`str`] + id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -73,7 +73,7 @@ def __init__( description: Optional[str] = None, spoiler: bool = False, row: Optional[int] = None, - id: Optional[str] = None, + id: Optional[int] = None, ) -> None: super().__init__() diff --git a/discord/ui/view.py b/discord/ui/view.py index bafcfedff4d0..9b0709fd4baa 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -223,6 +223,8 @@ def _init_children(self) -> List[Item[Self]]: parent = getattr(raw, '__discord_ui_parent__', None) if parent and parent._view is None: parent._view = self + if getattr(raw, '__pending_view__', False): + raw._update_children_view(self) # type: ignore children.append(raw) else: item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) @@ -581,6 +583,8 @@ def __init_subclass__(cls) -> None: for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): children[name] = member + elif isinstance(member, Item) and member._is_v2(): + raise RuntimeError(f'{name} cannot be added to this View') if len(children) > 25: raise TypeError('View cannot have more than 25 children') @@ -707,10 +711,14 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: def __init_subclass__(cls) -> None: children: Dict[str, Item[Any]] = {} + row = 0 + for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if isinstance(member, Item): + member._rendered_row = member._row or row children[name] = member + row += 1 elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): children[name] = member From 8cb80bf8f7824d8636581c8d12c5af16cfd0f0c9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 22:08:16 +0100 Subject: [PATCH 264/354] chore: improve check on container.__init_subclass__ --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index b60c1ec407fc..cc2405a757bb 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -140,7 +140,7 @@ def __init_subclass__(cls) -> None: for name, member in base.__dict__.items(): if isinstance(member, Item): children[name] = member - if hasattr(member, '__discord_ui_model_type__') and hasattr(member, '__discord_ui_parent__'): + if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): children[name] = member cls.__container_children_items__ = list(children.values()) From 7601533fe96bd2ee43c2a55eccbfe02fc433be97 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 22:35:37 +0100 Subject: [PATCH 265/354] chore: add id attr to components and black item.py --- discord/components.py | 112 +++++++++++++++++++++++++++++++++++---- discord/ui/action_row.py | 5 +- discord/ui/item.py | 3 +- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/discord/components.py b/discord/components.py index ef7d676700ac..a9a6de24ba5a 100644 --- a/discord/components.py +++ b/discord/components.py @@ -177,13 +177,18 @@ class ActionRow(Component): ------------ children: List[Union[:class:`Button`, :class:`SelectMenu`, :class:`TextInput`]] The children components that this holds, if any. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ - __slots__: Tuple[str, ...] = ('children',) + __slots__: Tuple[str, ...] = ('children', 'id') __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ActionRowPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.children: List[ActionRowChildComponentType] = [] for component_data in data.get('components', []): @@ -198,10 +203,13 @@ def type(self) -> Literal[ComponentType.action_row]: return ComponentType.action_row def to_dict(self) -> ActionRowPayload: - return { + payload: ActionRowPayload = { 'type': self.type.value, 'components': [child.to_dict() for child in self.children], } + if self.id is not None: + payload['id'] = self.id + return payload class Button(Component): @@ -235,6 +243,10 @@ class Button(Component): The SKU ID this button sends you to, if available. .. versionadded:: 2.4 + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -245,11 +257,13 @@ class Button(Component): 'label', 'emoji', 'sku_id', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ def __init__(self, data: ButtonComponentPayload, /) -> None: + self.id: Optional[int] = data.get('id') self.style: ButtonStyle = try_enum(ButtonStyle, data['style']) self.custom_id: Optional[str] = data.get('custom_id') self.url: Optional[str] = data.get('url') @@ -278,6 +292,9 @@ def to_dict(self) -> ButtonComponentPayload: 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id + if self.sku_id: payload['sku_id'] = str(self.sku_id) @@ -329,6 +346,10 @@ class SelectMenu(Component): Whether the select is disabled or not. channel_types: List[:class:`.ChannelType`] A list of channel types that are allowed to be chosen in this select menu. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -341,6 +362,7 @@ class SelectMenu(Component): 'disabled', 'channel_types', 'default_values', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -357,6 +379,7 @@ def __init__(self, data: SelectMenuPayload, /) -> None: self.default_values: List[SelectDefaultValue] = [ SelectDefaultValue.from_dict(d) for d in data.get('default_values', []) ] + self.id: Optional[int] = data.get('id') def to_dict(self) -> SelectMenuPayload: payload: SelectMenuPayload = { @@ -366,6 +389,8 @@ def to_dict(self) -> SelectMenuPayload: 'max_values': self.max_values, 'disabled': self.disabled, } + if self.id is not None: + payload['id'] = self.id if self.placeholder: payload['placeholder'] = self.placeholder if self.options: @@ -531,6 +556,10 @@ class TextInput(Component): The minimum length of the text input. max_length: Optional[:class:`int`] The maximum length of the text input. + id: Optional[:class:`int`] + The ID of this component. + + .. versionadded:: 2.6 """ __slots__: Tuple[str, ...] = ( @@ -542,6 +571,7 @@ class TextInput(Component): 'required', 'min_length', 'max_length', + 'id', ) __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ @@ -555,6 +585,7 @@ def __init__(self, data: TextInputPayload, /) -> None: self.required: bool = data.get('required', True) self.min_length: Optional[int] = data.get('min_length') self.max_length: Optional[int] = data.get('max_length') + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.text_input]: @@ -570,6 +601,9 @@ def to_dict(self) -> TextInputPayload: 'required': self.required, } + if self.id is not None: + payload['id'] = self.id + if self.placeholder: payload['placeholder'] = self.placeholder @@ -721,11 +755,14 @@ class SectionComponent(Component): The components on this section. accessory: :class:`Component` The section accessory. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'components', 'accessory', + 'id', ) __repr_info__ = __slots__ @@ -733,6 +770,7 @@ class SectionComponent(Component): def __init__(self, data: SectionComponentPayload, state: Optional[ConnectionState]) -> None: self.components: List[SectionComponentType] = [] self.accessory: Component = _component_factory(data['accessory'], state) # type: ignore + self.id: Optional[int] = data.get('id') for component_data in data['components']: component = _component_factory(component_data, state) @@ -749,6 +787,10 @@ def to_dict(self) -> SectionComponentPayload: 'components': [c.to_dict() for c in self.components], 'accessory': self.accessory.to_dict(), } + + if self.id is not None: + payload['id'] = self.id + return payload @@ -772,12 +814,15 @@ class ThumbnailComponent(Component): The description shown within this thumbnail. spoiler: :class:`bool` Whether this thumbnail is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'media', 'spoiler', 'description', + 'id', ) __repr_info__ = __slots__ @@ -790,19 +835,25 @@ def __init__( self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['media'], state) self.description: Optional[str] = data.get('description') self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.thumbnail]: return ComponentType.thumbnail def to_dict(self) -> ThumbnailComponentPayload: - return { - 'media': self.media.to_dict(), # pyright: ignore[reportReturnType] + payload = { + 'media': self.media.to_dict(), 'description': self.description, 'spoiler': self.spoiler, 'type': self.type.value, } + if self.id is not None: + payload['id'] = self.id + + return payload # type: ignore + class TextDisplay(Component): """Represents a text display from the Discord Bot UI Kit. @@ -820,24 +871,30 @@ class TextDisplay(Component): ---------- content: :class:`str` The content that this display shows. + id: Optional[:class:`int`] + The ID of this component. """ - __slots__ = ('content',) + __slots__ = ('content', 'id') __repr_info__ = __slots__ def __init__(self, data: TextComponentPayload) -> None: self.content: str = data['content'] + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.text_display]: return ComponentType.text_display def to_dict(self) -> TextComponentPayload: - return { + payload: TextComponentPayload = { 'type': self.type.value, 'content': self.content, } + if self.id is not None: + payload['id'] = self.id + return payload class UnfurledMediaItem(AssetMixin): @@ -1006,24 +1063,30 @@ class MediaGalleryComponent(Component): ---------- items: List[:class:`MediaGalleryItem`] The items this gallery has. + id: Optional[:class:`int`] + The ID of this component. """ - __slots__ = ('items',) + __slots__ = ('items', 'id') __repr_info__ = __slots__ def __init__(self, data: MediaGalleryComponentPayload, state: Optional[ConnectionState]) -> None: self.items: List[MediaGalleryItem] = MediaGalleryItem._from_gallery(data['items'], state) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.media_gallery]: return ComponentType.media_gallery def to_dict(self) -> MediaGalleryComponentPayload: - return { + payload: MediaGalleryComponentPayload = { 'type': self.type.value, 'items': [item.to_dict() for item in self.items], } + if self.id is not None: + payload['id'] = self.id + return payload class FileComponent(Component): @@ -1044,11 +1107,14 @@ class FileComponent(Component): The unfurled attachment contents of the file. spoiler: :class:`bool` Whether this file is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'media', 'spoiler', + 'id', ) __repr_info__ = __slots__ @@ -1056,17 +1122,21 @@ class FileComponent(Component): def __init__(self, data: FileComponentPayload, state: Optional[ConnectionState]) -> None: self.media: UnfurledMediaItem = UnfurledMediaItem._from_data(data['file'], state) self.spoiler: bool = data.get('spoiler', False) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.file]: return ComponentType.file def to_dict(self) -> FileComponentPayload: - return { + payload: FileComponentPayload = { 'type': self.type.value, 'file': self.media.to_dict(), # type: ignore 'spoiler': self.spoiler, } + if self.id is not None: + payload['id'] = self.id + return payload class SeparatorComponent(Component): @@ -1087,11 +1157,14 @@ class SeparatorComponent(Component): The spacing size of the separator. visible: :class:`bool` Whether this separator is visible and shows a divider. + id: Optional[:class:`int`] + The ID of this component. """ __slots__ = ( 'spacing', 'visible', + 'id', ) __repr_info__ = __slots__ @@ -1102,17 +1175,21 @@ def __init__( ) -> None: self.spacing: SeparatorSize = try_enum(SeparatorSize, data.get('spacing', 1)) self.visible: bool = data.get('divider', True) + self.id: Optional[int] = data.get('id') @property def type(self) -> Literal[ComponentType.separator]: return ComponentType.separator def to_dict(self) -> SeparatorComponentPayload: - return { + payload: SeparatorComponentPayload = { 'type': self.type.value, 'divider': self.visible, 'spacing': self.spacing.value, } + if self.id is not None: + payload['id'] = self.id + return payload class Container(Component): @@ -1133,10 +1210,13 @@ class Container(Component): This container's children. spoiler: :class:`bool` Whether this container is flagged as a spoiler. + id: Optional[:class:`int`] + The ID of this component. """ def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None: self.children: List[Component] = [] + self.id: Optional[int] = data.get('id') for child in data['components']: comp = _component_factory(child, state) @@ -1158,6 +1238,18 @@ def accent_colour(self) -> Optional[Colour]: accent_color = accent_colour + def to_dict(self) -> ContainerComponentPayload: + payload: ContainerComponentPayload = { + 'type': self.type.value, # type: ignore + 'spoiler': self.spoiler, + 'components': [c.to_dict() for c in self.children], + } + if self.id is not None: + payload['id'] = self.id + if self._colour: + payload['accent_color'] = self._colour.value + return payload + def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]: if data['type'] == 1: diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 510d6175bf1a..b13948899e91 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -234,10 +234,13 @@ def to_component_dict(self) -> Dict[str, Any]: for item in self._children: components.append(item.to_component_dict()) - return { + base = { 'type': self.type.value, 'components': components, } + if self.id is not None: + base['id'] = self.id + return base def button( self, diff --git a/discord/ui/item.py b/discord/ui/item.py index bcee854a87aa..854affa396f4 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -127,8 +127,7 @@ def view(self) -> Optional[V]: @property def id(self) -> Optional[int]: - """Optional[:class:`int`]: The ID of this component. - """ + """Optional[:class:`int`]: The ID of this component.""" return self._id @id.setter From 9891f85c8b8507bbc7ec7ae6667f43b0f5f6a054 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 22:40:15 +0100 Subject: [PATCH 266/354] chore: add id to every item --- discord/ui/button.py | 2 ++ discord/ui/container.py | 6 +++++- discord/ui/file.py | 2 ++ discord/ui/media_gallery.py | 2 ++ discord/ui/section.py | 3 +++ discord/ui/select.py | 2 ++ discord/ui/separator.py | 2 ++ discord/ui/text_display.py | 6 +++++- discord/ui/text_input.py | 8 ++++++++ discord/ui/thumbnail.py | 6 +++++- 10 files changed, 36 insertions(+), 3 deletions(-) diff --git a/discord/ui/button.py b/discord/ui/button.py index 82a485f9110f..7a60333db414 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -149,6 +149,7 @@ def __init__( style=style, emoji=emoji, sku_id=sku_id, + id=id, ) self._parent: Optional[ActionRow] = None self.row = row @@ -250,6 +251,7 @@ def from_component(cls, button: ButtonComponent) -> Self: emoji=button.emoji, row=None, sku_id=button.sku_id, + id=button.id, ) @property diff --git a/discord/ui/container.py b/discord/ui/container.py index cc2405a757bb..b4aa574b616b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -194,12 +194,15 @@ def to_components(self) -> List[Dict[str, Any]]: def to_component_dict(self) -> Dict[str, Any]: components = self.to_components() - return { + base = { 'type': self.type.value, 'accent_color': self._colour.value if self._colour else None, 'spoiler': self.spoiler, 'components': components, } + if self.id is not None: + base['id'] = self.id + return base def _update_store_data( self, @@ -222,4 +225,5 @@ def from_component(cls, component: ContainerComponent) -> Self: children=[_component_to_item(c) for c in component.children], accent_colour=component.accent_colour, spoiler=component.spoiler, + id=component.id, ) diff --git a/discord/ui/file.py b/discord/ui/file.py index 7d065f0ffcbb..2e34c316d687 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -75,6 +75,7 @@ def __init__( self._underlying = FileComponent._raw_construct( media=UnfurledMediaItem(media) if isinstance(media, str) else media, spoiler=spoiler, + id=id, ) self.row = row @@ -126,4 +127,5 @@ def from_component(cls, component: FileComponent) -> Self: return cls( media=component.media, spoiler=component.spoiler, + id=component.id, ) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index ee0fb3cf0cb0..3deca63c86cf 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -75,6 +75,7 @@ def __init__( self._underlying = MediaGalleryComponent._raw_construct( items=items, + id=id, ) self.row = row @@ -183,4 +184,5 @@ def width(self): def from_component(cls, component: MediaGalleryComponent) -> Self: return cls( items=component.items, + id=component.id, ) diff --git a/discord/ui/section.py b/discord/ui/section.py index 0aa164d883af..a034a1c08781 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -174,6 +174,7 @@ def from_component(cls, component: SectionComponent) -> Self: return cls( children=[_component_to_item(c) for c in component.components], accessory=_component_to_item(component.accessory), + id=component.id, ) def to_component_dict(self) -> Dict[str, Any]: @@ -182,4 +183,6 @@ def to_component_dict(self) -> Dict[str, Any]: 'components': [c.to_component_dict() for c in self._children], 'accessory': self.accessory.to_component_dict(), } + if self.id is not None: + data['id'] = self.id return data diff --git a/discord/ui/select.py b/discord/ui/select.py index efa8a9e6880e..e2d3d34d2583 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -224,6 +224,7 @@ class BaseSelect(Item[V]): 'min_values', 'max_values', 'disabled', + 'id', ) def __init__( @@ -257,6 +258,7 @@ def __init__( channel_types=[] if channel_types is MISSING else channel_types, options=[] if options is MISSING else options, default_values=[] if default_values is MISSING else default_values, + id=id, ) self.row = row diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 394e9ac78df5..e212f4b4e4e5 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -74,6 +74,7 @@ def __init__( self._underlying = SeparatorComponent._raw_construct( spacing=spacing, visible=visible, + id=id, ) self.row = row @@ -120,4 +121,5 @@ def from_component(cls, component: SeparatorComponent) -> Self: return cls( visible=component.visible, spacing=component.spacing, + id=component.id, ) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 8e22905ebc47..409b68272187 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -67,10 +67,13 @@ def __init__(self, content: str, *, row: Optional[int] = None, id: Optional[int] self.id = id def to_component_dict(self): - return { + base = { 'type': self.type.value, 'content': self.content, } + if self.id is not None: + base['id'] = self.id + return base @property def width(self): @@ -87,4 +90,5 @@ def _is_v2(self) -> bool: def from_component(cls, component: TextDisplayComponent) -> Self: return cls( content=component.content, + id=component.id, ) diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 96b4581f40b0..86f7373ee11e 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -92,6 +92,10 @@ class TextInput(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of the component. This must be unique across the view. + + .. versionadded:: 2.6 """ __item_repr_attributes__: Tuple[str, ...] = ( @@ -112,6 +116,7 @@ def __init__( min_length: Optional[int] = None, max_length: Optional[int] = None, row: Optional[int] = None, + id: Optional[int] = None, ) -> None: super().__init__() self._value: Optional[str] = default @@ -129,8 +134,10 @@ def __init__( required=required, min_length=min_length, max_length=max_length, + id=id, ) self.row = row + self.id = id def __str__(self) -> str: return self.value @@ -241,6 +248,7 @@ def from_component(cls, component: TextInputComponent) -> Self: min_length=component.min_length, max_length=component.max_length, row=None, + id=component.id, ) @property diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index e9a2c13f5a9b..7f21edd3aad7 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -96,12 +96,15 @@ def _is_v2(self) -> bool: return True def to_component_dict(self) -> Dict[str, Any]: - return { + base = { 'type': self.type.value, 'spoiler': self.spoiler, 'media': self.media.to_dict(), 'description': self.description, } + if self.id is not None: + base['id'] = self.id + return base @classmethod def from_component(cls, component: ThumbnailComponent) -> Self: @@ -109,4 +112,5 @@ def from_component(cls, component: ThumbnailComponent) -> Self: media=component.media.url, description=component.description, spoiler=component.spoiler, + id=component.id, ) From c93ee07ca9654a486bd59b9349cabcab5ecafe9b Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:11:27 +0100 Subject: [PATCH 267/354] fix: Container._colour raising ValueError --- discord/components.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/discord/components.py b/discord/components.py index a9a6de24ba5a..5ed52891fa83 100644 --- a/discord/components.py +++ b/discord/components.py @@ -1225,11 +1225,11 @@ def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionSt self.children.append(comp) self.spoiler: bool = data.get('spoiler', False) - self._colour: Optional[Colour] - try: - self._colour = Colour(data['accent_color']) # type: ignore - except KeyError: - self._colour = None + + colour = data.get('accent_color') + self._colour: Optional[Colour] = None + if colour is not None: + self._colour = Colour(colour) @property def accent_colour(self) -> Optional[Colour]: From 09fceae041a8957a41c6dce6f97e4788494385cd Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:12:55 +0100 Subject: [PATCH 268/354] fix: Container.is_dispatchable making buttons not work --- discord/ui/container.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index b4aa574b616b..256e3cfc5d4e 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -183,9 +183,6 @@ def width(self): def _is_v2(self) -> bool: return True - def is_dispatchable(self) -> bool: - return any(c.is_dispatchable() for c in self.children) - def to_components(self) -> List[Dict[str, Any]]: components = [] for child in self._children: From 8399677445d34377f6804055b641a8cf5d0561a1 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:14:59 +0100 Subject: [PATCH 269/354] fix: Container children not being added to view store --- discord/ui/container.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index 256e3cfc5d4e..9750010ad44b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -85,6 +85,7 @@ class Container(Item[V]): __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] __pending_view__: ClassVar[bool] = True + __discord_ui_container__: ClassVar[bool] = True def __init__( self, @@ -132,6 +133,9 @@ def _init_children(self) -> List[Item[Any]]: return children + def is_dispatchable(self) -> bool: + return True + def __init_subclass__(cls) -> None: super().__init_subclass__() From 97006066c06de47d995975c02b53955b4ca74818 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:22:37 +0100 Subject: [PATCH 270/354] chore: Update Container._update_store_data --- discord/ui/container.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 9750010ad44b..d546d593ad47 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -97,6 +97,7 @@ def __init__( row: Optional[int] = None, id: Optional[int] = None, ) -> None: + self.__dispatchable: List[Item[V]] = [] self._children: List[Item[V]] = self._init_children() if children is not MISSING: @@ -130,6 +131,7 @@ def _init_children(self) -> List[Item[Any]]: parent._children.append(item) # we donnot append it to the children list because technically these buttons and # selects are not from the container but the action row itself. + self.__dispatchable.append(item) return children @@ -211,7 +213,7 @@ def _update_store_data( dynamic_items: Dict[Any, Type[DynamicItem]], ) -> bool: is_fully_dynamic = True - for item in self._children: + for item in self.__dispatchable: if isinstance(item, DynamicItem): pattern = item.__discord_ui_compiled_template__ dynamic_items[pattern] = item.__class__ From 0f7d72bc0bf533801de5c3716dd161c25582f789 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 9 Mar 2025 23:24:33 +0100 Subject: [PATCH 271/354] chore: Update Container.is_dispatchable --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index d546d593ad47..0376895c0ac5 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -136,7 +136,7 @@ def _init_children(self) -> List[Item[Any]]: return children def is_dispatchable(self) -> bool: - return True + return bool(self.__dispatchable) def __init_subclass__(cls) -> None: super().__init_subclass__() From cf4db91fa256c81ea82455c04933cb1da7ea0c48 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 00:04:55 +0100 Subject: [PATCH 272/354] chore: Remove unused imports --- discord/ui/action_row.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index b13948899e91..74dc151cea2e 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -23,7 +23,6 @@ """ from __future__ import annotations -import os from typing import ( TYPE_CHECKING, Any, From 6d50c883abd8e1c72b6fa2a9236fcd246a4e6f98 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 00:38:39 +0100 Subject: [PATCH 273/354] chore: Metadata for Section --- discord/ui/section.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index a034a1c08781..bbaef2994948 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union, ClassVar from .item import Item from .text_display import TextDisplay @@ -63,6 +63,8 @@ class Section(Item[V]): The ID of this component. This must be unique across the view. """ + __discord_ui_section__: ClassVar[bool] = True + __slots__ = ( '_children', 'accessory', From 9655749ae33b6347dff7f872461064190f109775 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 00:41:25 +0100 Subject: [PATCH 274/354] fix: Section.accessory not being dispatched --- discord/ui/view.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/ui/view.py b/discord/ui/view.py index 9b0709fd4baa..1acf58870c0c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -844,6 +844,9 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: ) or is_fully_dynamic ) + elif getattr(item, '__discord_ui_section__', False): + accessory = item.accessory. # type: ignore + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 5120b0d5dfa40fe8ef30b64cde20a3bfd8a61c74 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 09:50:10 +0100 Subject: [PATCH 275/354] chore: Update ViewStore to handle Section.accessory properly --- discord/ui/view.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 1acf58870c0c..1eadf0a8b364 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -845,8 +845,12 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: or is_fully_dynamic ) elif getattr(item, '__discord_ui_section__', False): - accessory = item.accessory. # type: ignore - dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore + accessory = item.accessory # type: ignore + if isinstance(accessory, DynamicItem): + pattern = accessory.__discord_ui_compiled_pattern__ + self._dynamic_items[pattern] = accessory.__class__ + else: + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 4c668bae5736adcce29bb12019a8cfcfd909aac0 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 09:54:25 +0100 Subject: [PATCH 276/354] template --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 1eadf0a8b364..e3771a8fe82f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -847,7 +847,7 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: elif getattr(item, '__discord_ui_section__', False): accessory = item.accessory # type: ignore if isinstance(accessory, DynamicItem): - pattern = accessory.__discord_ui_compiled_pattern__ + pattern = accessory.__discord_ui_compiled_template__ self._dynamic_items[pattern] = accessory.__class__ else: dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore From 84ad47ffc269e528e9fa91ccca331f8e46279892 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 17:20:04 +0100 Subject: [PATCH 277/354] chore: Remove unneccessary # type: ignore --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index e3771a8fe82f..dcd9d90e797f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -850,7 +850,7 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: pattern = accessory.__discord_ui_compiled_template__ self._dynamic_items[pattern] = accessory.__class__ else: - dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 7433ad05623d73db2c610d744d29b42656ff6007 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:32:12 +0100 Subject: [PATCH 278/354] chore: Fix Section.accessory raising an error when clicked --- discord/ui/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index dcd9d90e797f..7016ef9d606d 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -845,12 +845,14 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: or is_fully_dynamic ) elif getattr(item, '__discord_ui_section__', False): - accessory = item.accessory # type: ignore + accessory: Item = item.accessory # type: ignore + accessory._view = view + if isinstance(accessory, DynamicItem): pattern = accessory.__discord_ui_compiled_template__ self._dynamic_items[pattern] = accessory.__class__ else: - dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore else: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False From 810fe57283ba0264eb9ac9d7ef6960496504ddce Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:37:11 +0100 Subject: [PATCH 279/354] chore: Update container to also take in account section accessories --- discord/ui/container.py | 8 ++++++-- discord/ui/section.py | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 0376895c0ac5..810f05b55816 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -218,8 +218,12 @@ def _update_store_data( pattern = item.__discord_ui_compiled_template__ dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore - is_fully_dynamic = False + if getattr(item, '__discord_ui_section__', False): + accessory = item.accessory # type: ignore + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory + else: + dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + is_fully_dynamic = False return is_fully_dynamic @classmethod diff --git a/discord/ui/section.py b/discord/ui/section.py index bbaef2994948..1cd972d5dec9 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -64,6 +64,7 @@ class Section(Item[V]): """ __discord_ui_section__: ClassVar[bool] = True + __pending_view__: ClassVar[bool] = True __slots__ = ( '_children', @@ -107,9 +108,10 @@ def _is_v2(self) -> bool: # be accessory component callback, only called if accessory is # dispatchable? def is_dispatchable(self) -> bool: - if self.accessory: - return self.accessory.is_dispatchable() - return False + return self.accessory.is_dispatchable() + + def _update_children_view(self, view) -> None: + self.accessory._view = view def add_item(self, item: Union[str, Item[Any]]) -> Self: """Adds an item to this section. From 52f9b6a88c3a6c28ebad428fb7b445bd93f5440b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:40:12 +0100 Subject: [PATCH 280/354] chore: Some changes on how Section.accessory is handled in Container --- discord/ui/container.py | 11 +++++------ discord/ui/section.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 810f05b55816..8218e1de6ebe 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -116,6 +116,9 @@ def _init_children(self) -> List[Item[Any]]: for raw in self.__container_children_items__: if isinstance(raw, Item): children.append(raw) + + if getattr(raw, '__discord_ui_section__', False) and raw.accessory.is_dispatchable(): # type: ignore + self.__dispatchable.append(raw.accessory) # type: ignore else: # action rows can be created inside containers, and then callbacks can exist here # so we create items based off them @@ -218,12 +221,8 @@ def _update_store_data( pattern = item.__discord_ui_compiled_template__ dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - if getattr(item, '__discord_ui_section__', False): - accessory = item.accessory # type: ignore - dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory - else: - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore - is_fully_dynamic = False + dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + is_fully_dynamic = False return is_fully_dynamic @classmethod diff --git a/discord/ui/section.py b/discord/ui/section.py index 1cd972d5dec9..981d06e93928 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -64,7 +64,6 @@ class Section(Item[V]): """ __discord_ui_section__: ClassVar[bool] = True - __pending_view__: ClassVar[bool] = True __slots__ = ( '_children', From 8561953222c29248e1c7755e8add398852b5b5c4 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:54:33 +0100 Subject: [PATCH 281/354] chore: Add container add/remove/clear_item(s) --- discord/ui/container.py | 60 +++++++++++++++++++++++++++++++++++++++++ discord/ui/section.py | 2 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 8218e1de6ebe..fd9dd0c49e7b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -233,3 +233,63 @@ def from_component(cls, component: ContainerComponent) -> Self: spoiler=component.spoiler, id=component.id, ) + + def add_item(self, item: Item[Any]) -> Self: + """Adds an item to this container. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`Item` + The item to append. + + Raises + ------ + TypeError + An :class:`Item` was not passed. + ValueError + Maximum number of children has been exceeded (10). + """ + + if len(self._children) >= 10: + raise ValueError('maximum number of children exceeded') + + if not isinstance(item, Item): + raise TypeError(f'expected Item not {item.__class__.__name__}') + + self._children.append(item) + + if item.is_dispatchable(): + if getattr(item, '__discord_ui_section__', False): + self.__dispatchable.append(item.accessory) # type: ignore + + return self + + def remove_item(self, item: Item[Any]) -> Self: + """Removes an item from this container. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + item: :class:`TextDisplay` + The item to remove from the section. + """ + + try: + self._children.remove(item) + except ValueError: + pass + return self + + def clear_items(self) -> Self: + """Removes all the items from the container. + + This function returns the class instance to allow for fluent-style + chaining. + """ + self._children.clear() + return self diff --git a/discord/ui/section.py b/discord/ui/section.py index 981d06e93928..16b5fb6c52df 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -121,7 +121,7 @@ def add_item(self, item: Union[str, Item[Any]]) -> Self: Parameters ---------- item: Union[:class:`str`, :class:`Item`] - The items to append, if it is a string it automatically wrapped around + The item to append, if it is a string it automatically wrapped around :class:`TextDisplay`. Raises From 8926f28a3a756f5e08c4db55f0d74439217f0fed Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:00:04 +0100 Subject: [PATCH 282/354] fix: Section.accessory._view being None when in a container --- discord/ui/section.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/section.py b/discord/ui/section.py index 16b5fb6c52df..be53d662001c 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -64,6 +64,7 @@ class Section(Item[V]): """ __discord_ui_section__: ClassVar[bool] = True + __pending_view__: ClassVar[bool] = True __slots__ = ( '_children', From b1e8aefd538d2141037975c7f23842ab81702bcd Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:21:43 +0100 Subject: [PATCH 283/354] fix: Containers not dispatching ActionRow items correctly --- discord/ui/container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index fd9dd0c49e7b..1e4aa0bf7ce9 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -119,6 +119,8 @@ def _init_children(self) -> List[Item[Any]]: if getattr(raw, '__discord_ui_section__', False) and raw.accessory.is_dispatchable(): # type: ignore self.__dispatchable.append(raw.accessory) # type: ignore + elif getattr(raw, '__discord_ui_action_row__', False) and raw.is_dispatchable(): + self.__dispatchable.extend(raw._children) # type: ignore else: # action rows can be created inside containers, and then callbacks can exist here # so we create items based off them From 4c662a9c24593cea1e21048d8567e80d884b722b Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:58:43 +0100 Subject: [PATCH 284/354] chore: Some changes, fixes, and typo corrections --- discord/ui/action_row.py | 2 +- discord/ui/container.py | 4 ++-- discord/ui/file.py | 2 +- discord/ui/item.py | 8 ++++++++ discord/ui/section.py | 2 +- discord/ui/view.py | 28 +++++++++++++++++----------- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 74dc151cea2e..4edf78b603f3 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -99,7 +99,7 @@ class ActionRow(Item[V]): __action_row_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] __discord_ui_action_row__: ClassVar[bool] = True - __pending_view__: ClassVar[bool] = True + __discord_ui_update_view__: ClassVar[bool] = True def __init__(self, *, id: Optional[int] = None) -> None: super().__init__() diff --git a/discord/ui/container.py b/discord/ui/container.py index fd9dd0c49e7b..7a034e3c60be 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -84,7 +84,7 @@ class Container(Item[V]): """ __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] - __pending_view__: ClassVar[bool] = True + __discord_ui_update_view__: ClassVar[bool] = True __discord_ui_container__: ClassVar[bool] = True def __init__( @@ -157,7 +157,7 @@ def __init_subclass__(cls) -> None: def _update_children_view(self, view) -> None: for child in self._children: child._view = view - if getattr(child, '__pending_view__', False): + if getattr(child, '__discord_ui_update_view__', False): # if the item is an action row which child's view can be updated, then update it child._update_children_view(view) # type: ignore diff --git a/discord/ui/file.py b/discord/ui/file.py index 2e34c316d687..0f6875421521 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -47,7 +47,7 @@ class File(Item[V]): Parameters ---------- media: Union[:class:`str`, :class:`.UnfurledMediaItem`] - This file's media. If this is a string itmust point to a local + This file's media. If this is a string it must point to a local file uploaded within the parent view of this item, and must meet the ``attachment://file-name.extension`` structure. spoiler: :class:`bool` diff --git a/discord/ui/item.py b/discord/ui/item.py index 854affa396f4..597be4dab11b 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -53,6 +53,14 @@ class Item(Generic[V]): - :class:`discord.ui.Button` - :class:`discord.ui.Select` - :class:`discord.ui.TextInput` + - :class:`discord.ui.ActionRow` + - :class:`discord.ui.Container` + - :class:`discord.ui.File` + - :class:`discord.ui.MediaGallery` + - :class:`discord.ui.Section` + - :class:`discord.ui.Separator` + - :class:`discord.ui.TextDisplay` + - :class:`discord.ui.Thumbnail` .. versionadded:: 2.0 """ diff --git a/discord/ui/section.py b/discord/ui/section.py index be53d662001c..88fe03e5dcfe 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -64,7 +64,7 @@ class Section(Item[V]): """ __discord_ui_section__: ClassVar[bool] = True - __pending_view__: ClassVar[bool] = True + __discord_ui_update_view__: ClassVar[bool] = True __slots__ = ( '_children', diff --git a/discord/ui/view.py b/discord/ui/view.py index 7016ef9d606d..4a3e2ac8d730 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -23,6 +23,8 @@ """ from __future__ import annotations + +import warnings from typing import ( Any, Callable, @@ -183,10 +185,10 @@ def v2_weights(self) -> bool: class _ViewCallback: __slots__ = ('view', 'callback', 'item') - def __init__(self, callback: ItemCallbackType[Any, Any], view: View, item: Item[View]) -> None: + def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None: self.callback: ItemCallbackType[Any, Any] = callback - self.view: View = view - self.item: Item[View] = item + self.view: BaseView = view + self.item: Item[BaseView] = item def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: return self.callback(self.view, interaction, self.item) @@ -223,7 +225,7 @@ def _init_children(self) -> List[Item[Self]]: parent = getattr(raw, '__discord_ui_parent__', None) if parent and parent._view is None: parent._view = self - if getattr(raw, '__pending_view__', False): + if getattr(raw, '__discord_ui_update_view__', False): raw._update_children_view(self) # type: ignore children.append(raw) else: @@ -559,13 +561,15 @@ async def wait(self) -> bool: return await self.__stopped -class View(BaseView): # NOTE: maybe add a deprecation warning in favour of LayoutView? +class View(BaseView): """Represents a UI view. This object must be inherited to create a UI within Discord. .. versionadded:: 2.0 + .. deprecated:: 2.6 + Parameters ----------- timeout: Optional[:class:`float`] @@ -576,6 +580,10 @@ class View(BaseView): # NOTE: maybe add a deprecation warning in favour of Layo __discord_ui_view__: ClassVar[bool] = True def __init_subclass__(cls) -> None: + warnings.warn( + 'discord.ui.View and subclasses are deprecated, use discord.ui.LayoutView instead', + DeprecationWarning, + ) super().__init_subclass__() children: Dict[str, ItemCallbackType[Any, Any]] = {} @@ -691,10 +699,7 @@ def clear_items(self) -> Self: class LayoutView(BaseView): - """Represents a layout view for components v2. - - Unline :class:`View` this allows for components v2 to exist - within it. + """Represents a layout view for components. .. versionadded:: 2.6 @@ -710,6 +715,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: def __init_subclass__(cls) -> None: children: Dict[str, Item[Any]] = {} + callback_children: Dict[str, ItemCallbackType[Any, Any]] = {} row = 0 @@ -720,12 +726,12 @@ def __init_subclass__(cls) -> None: children[name] = member row += 1 elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): - children[name] = member + callback_children[name] = member if len(children) > 10: raise TypeError('LayoutView cannot have more than 10 top-level children') - cls.__view_children_items__ = list(children.values()) + cls.__view_children_items__ = list(children.values()) + list(callback_children.values()) def _is_v2(self) -> bool: return True From 4ef1e4642637e251af2d7ae3cf028ae954963bf9 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 20 Mar 2025 19:02:37 +0100 Subject: [PATCH 285/354] chore: Add ActionRow to docs --- docs/interactions/api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index a4005882341b..b75d33044b71 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -778,6 +778,16 @@ Thumbnail :members: :inherited-members: + +ActionRow +~~~~~~~~~ + +.. attributetable:: discord.ui.ActionRow + +.. autoclass:: discord.ui.ActionRow + :members: + :inherited-members: + .. _discord_app_commands: Application Commands From 86dd8d8b9ab3230267453d634af3f315f1b782ef Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:06:20 +0200 Subject: [PATCH 286/354] chore: Add get_item_by_id to remaining items --- discord/ui/action_row.py | 22 +++++++++++++++++++++- discord/ui/container.py | 22 +++++++++++++++++++++- discord/ui/section.py | 22 +++++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 4edf78b603f3..481337384b7d 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -47,7 +47,7 @@ from ..components import ActionRow as ActionRowComponent from ..enums import ButtonStyle, ComponentType, ChannelType from ..partial_emoji import PartialEmoji -from ..utils import MISSING +from ..utils import MISSING, get as _utils_get if TYPE_CHECKING: from typing_extensions import Self @@ -218,6 +218,26 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self + def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`str` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + def clear_items(self) -> Self: """Removes all items from the row. diff --git a/discord/ui/container.py b/discord/ui/container.py index 40583e17afba..20aff903c09c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -29,7 +29,7 @@ from .view import _component_to_item, LayoutView from .dynamic import DynamicItem from ..enums import ComponentType -from ..utils import MISSING +from ..utils import MISSING, get as _utils_get if TYPE_CHECKING: from typing_extensions import Self @@ -287,6 +287,26 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self + def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`str` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + def clear_items(self) -> Self: """Removes all the items from the container. diff --git a/discord/ui/section.py b/discord/ui/section.py index 88fe03e5dcfe..5a3104af8fab 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -28,7 +28,7 @@ from .item import Item from .text_display import TextDisplay from ..enums import ComponentType -from ..utils import MISSING +from ..utils import MISSING, get as _utils_get if TYPE_CHECKING: from typing_extensions import Self @@ -162,6 +162,26 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self + def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if + not found. + + .. warning:: + + This is **not the same** as ``custom_id``. + + Parameters + ---------- + id: :class:`str` + The ID of the component. + + Returns + ------- + Optional[:class:`Item`] + The item found, or ``None``. + """ + return _utils_get(self._children, id=id) + def clear_items(self) -> Self: """Removes all the items from the section. From cd9f7768fb37d537586f6815c423efd575444472 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:53:35 +0200 Subject: [PATCH 287/354] some fixes and typings --- discord/ui/action_row.py | 6 +++--- discord/ui/container.py | 7 ++++--- discord/ui/item.py | 7 +++++++ discord/ui/section.py | 12 +++++++++--- discord/ui/view.py | 4 ++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 481337384b7d..70384cf9a0b7 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -140,7 +140,7 @@ def _update_store_data(self, dispatch_info: Dict, dynamic_items: Dict) -> bool: pattern = item.__discord_ui_compiled_template__ dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + dispatch_info[(item.type.value, item.custom_id)] = item is_fully_dynamic = False return is_fully_dynamic @@ -218,7 +218,7 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self - def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -228,7 +228,7 @@ def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: Parameters ---------- - id: :class:`str` + id: :class:`int` The ID of the component. Returns diff --git a/discord/ui/container.py b/discord/ui/container.py index 20aff903c09c..198973dbeedc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -97,6 +97,7 @@ def __init__( row: Optional[int] = None, id: Optional[int] = None, ) -> None: + super().__init__() self.__dispatchable: List[Item[V]] = [] self._children: List[Item[V]] = self._init_children() @@ -196,7 +197,7 @@ def _is_v2(self) -> bool: def to_components(self) -> List[Dict[str, Any]]: components = [] - for child in self._children: + for child in sorted(self._children, key=lambda i: i._rendered_row or 0): components.append(child.to_component_dict()) return components @@ -287,7 +288,7 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self - def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -297,7 +298,7 @@ def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: Parameters ---------- - id: :class:`str` + id: :class:`int` The ID of the component. Returns diff --git a/discord/ui/item.py b/discord/ui/item.py index 597be4dab11b..614859d726d2 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -24,6 +24,7 @@ from __future__ import annotations +import os from typing import Any, Callable, Coroutine, Dict, Generic, Optional, TYPE_CHECKING, Tuple, Type, TypeVar from ..interactions import Interaction @@ -81,6 +82,9 @@ def __init__(self): self._id: Optional[int] = None self._max_row: int = 5 if not self._is_v2() else 10 + if self._is_v2(): + self.custom_id: str = os.urandom(16).hex() + def to_component_dict(self) -> Dict[str, Any]: raise NotImplementedError @@ -124,6 +128,9 @@ def row(self, value: Optional[int]) -> None: else: raise ValueError(f'row cannot be negative or greater than or equal to {self._max_row}') + if self._rendered_row is None: + self._rendered_row = value + @property def width(self) -> int: return 1 diff --git a/discord/ui/section.py b/discord/ui/section.py index 5a3104af8fab..bacda788329d 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -162,7 +162,7 @@ def remove_item(self, item: Item[Any]) -> Self: pass return self - def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: + def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -172,7 +172,7 @@ def get_item_by_id(self, id: str, /) -> Optional[Item[V]]: Parameters ---------- - id: :class:`str` + id: :class:`int` The ID of the component. Returns @@ -204,7 +204,13 @@ def from_component(cls, component: SectionComponent) -> Self: def to_component_dict(self) -> Dict[str, Any]: data = { 'type': self.type.value, - 'components': [c.to_component_dict() for c in self._children], + 'components': [ + c.to_component_dict() for c in + sorted( + self._children, + key=lambda i: i._rendered_row or 0, + ) + ], 'accessory': self.accessory.to_component_dict(), } if self.id is not None: diff --git a/discord/ui/view.py b/discord/ui/view.py index 4a3e2ac8d730..e9bd6f773ac0 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -372,7 +372,7 @@ def clear_items(self) -> Self: self._children.clear() return self - def get_item_by_id(self, id: str, /) -> Optional[Item[Self]]: + def get_item_by_id(self, id: int, /) -> Optional[Item[Self]]: """Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if not found. @@ -384,7 +384,7 @@ def get_item_by_id(self, id: str, /) -> Optional[Item[Self]]: Parameters ---------- - id: :class:`str` + id: :class:`int` The ID of the component. Returns From 5dddf65c4b653b7deca9977cfa14e0e529d6f229 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:54:45 +0200 Subject: [PATCH 288/354] run black --- discord/ui/section.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index bacda788329d..13d13169cb15 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -205,8 +205,8 @@ def to_component_dict(self) -> Dict[str, Any]: data = { 'type': self.type.value, 'components': [ - c.to_component_dict() for c in - sorted( + c.to_component_dict() + for c in sorted( self._children, key=lambda i: i._rendered_row or 0, ) From a1216e7c365805911aca3bcf6feeb767adaa9734 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:59:44 +0200 Subject: [PATCH 289/354] fix error when using Message.components --- discord/components.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/discord/components.py b/discord/components.py index 5ed52891fa83..80842f7fcff1 100644 --- a/discord/components.py +++ b/discord/components.py @@ -1214,6 +1214,20 @@ class Container(Component): The ID of this component. """ + __slots__ = ( + 'children', + 'id', + 'spoiler', + '_colour', + ) + + __repr_info__ = ( + 'children', + 'id', + 'spoiler', + 'accent_colour', + ) + def __init__(self, data: ContainerComponentPayload, state: Optional[ConnectionState]) -> None: self.children: List[Component] = [] self.id: Optional[int] = data.get('id') From cba602d472d0a53811d444e6d0f6d9346081ce96 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:28:07 +0200 Subject: [PATCH 290/354] chore: Add more params to MessageFlags.components_v2 docstring --- discord/flags.py | 4 +++- discord/ui/view.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/discord/flags.py b/discord/flags.py index 1a9d612aac81..8bf4ee9c7326 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -503,7 +503,9 @@ def forwarded(self): def components_v2(self): """:class:`bool`: Returns ``True`` if the message has Discord's v2 components. - Does not allow sending any ``content``, ``embed``, or ``embeds``. + Does not allow sending any ``content``, ``embed``, ``embeds``, ``stickers``, or ``poll``. + + .. versionadded:: 2.6 """ return 32768 diff --git a/discord/ui/view.py b/discord/ui/view.py index e9bd6f773ac0..44a956b73207 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -567,8 +567,8 @@ class View(BaseView): This object must be inherited to create a UI within Discord. .. versionadded:: 2.0 - .. deprecated:: 2.6 + This class is deprecated and will be removed in a future version. Use :class:`LayoutView` instead. Parameters ----------- @@ -581,7 +581,8 @@ class View(BaseView): def __init_subclass__(cls) -> None: warnings.warn( - 'discord.ui.View and subclasses are deprecated, use discord.ui.LayoutView instead', + 'discord.ui.View and subclasses are deprecated and will be removed in' + 'a future version, use discord.ui.LayoutView instead', DeprecationWarning, ) super().__init_subclass__() From e9d942b233b7ea41bc33f7da1730eeb66715cdf7 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:39:10 +0200 Subject: [PATCH 291/354] chore: typings --- discord/ui/container.py | 2 +- discord/ui/dynamic.py | 4 ++-- discord/ui/view.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 198973dbeedc..c10d24119e72 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -224,7 +224,7 @@ def _update_store_data( pattern = item.__discord_ui_compiled_template__ dynamic_items[pattern] = item.__class__ elif item.is_dispatchable(): - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + dispatch_info[(item.type.value, item.custom_id)] = item is_fully_dynamic = False return is_fully_dynamic diff --git a/discord/ui/dynamic.py b/discord/ui/dynamic.py index ee3ad30d50c1..b8aa78fdbe30 100644 --- a/discord/ui/dynamic.py +++ b/discord/ui/dynamic.py @@ -144,7 +144,7 @@ def is_persistent(self) -> bool: @property def custom_id(self) -> str: """:class:`str`: The ID of the dynamic item that gets received during an interaction.""" - return self.item.custom_id # type: ignore # This attribute exists for dispatchable items + return self.item.custom_id @custom_id.setter def custom_id(self, value: str) -> None: @@ -154,7 +154,7 @@ def custom_id(self, value: str) -> None: if not self.template.match(value): raise ValueError(f'custom_id must match the template {self.template.pattern!r}') - self.item.custom_id = value # type: ignore # This attribute exists for dispatchable items + self.item.custom_id = value self._provided_custom_id = True @property diff --git a/discord/ui/view.py b/discord/ui/view.py index 44a956b73207..5107716bd3b3 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -492,7 +492,7 @@ def _dispatch_item(self, item: Item, interaction: Interaction): def _refresh(self, components: List[Component]) -> None: # fmt: off old_state: Dict[str, Item[Any]] = { - item.custom_id: item # type: ignore + item.custom_id: item for item in self._children if item.is_dispatchable() } @@ -859,9 +859,9 @@ def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None: pattern = accessory.__discord_ui_compiled_template__ self._dynamic_items[pattern] = accessory.__class__ else: - dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory # type: ignore + dispatch_info[(accessory.type.value, accessory.custom_id)] = accessory else: - dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore + dispatch_info[(item.type.value, item.custom_id)] = item is_fully_dynamic = False view._cache_key = message_id @@ -880,7 +880,7 @@ def remove_view(self, view: View) -> None: pattern = item.__discord_ui_compiled_template__ self._dynamic_items.pop(pattern, None) elif item.is_dispatchable(): - dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore + dispatch_info.pop((item.type.value, item.custom_id), None) if len(dispatch_info) == 0: self._views.pop(view._cache_key, None) From ec186ab18f0c605a6be25e8e705b04a31b1cb6c0 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 00:13:27 +0200 Subject: [PATCH 292/354] chore: update docstrings --- discord/abc.py | 4 +++- discord/channel.py | 5 ++++- discord/client.py | 7 +++++-- discord/webhook/async_.py | 4 +++- discord/webhook/sync.py | 4 +++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index b7fd0252ef91..748a021d7113 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1546,10 +1546,12 @@ async def send( If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. .. versionadded:: 1.6 - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This parameter now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. diff --git a/discord/channel.py b/discord/channel.py index 3dc43d388549..8833f566ea13 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2907,8 +2907,11 @@ async def create_thread( If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. applied_tags: List[:class:`discord.ForumTag`] A list of tags to apply to the thread. - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. + + .. versionchanged:: 2.6 + This parameter now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. suppress_embeds: :class:`bool` diff --git a/discord/client.py b/discord/client.py index 2eaae2455b76..c620dc23a9b5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -3159,8 +3159,11 @@ def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None: Parameters ------------ - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] The view to register for dispatching. + + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. message_id: Optional[:class:`int`] The message ID that the view is attached to. This is currently used to refresh the view's state during message update events. If not given @@ -3188,7 +3191,7 @@ def add_view(self, view: BaseView, *, message_id: Optional[int] = None) -> None: @property def persistent_views(self) -> Sequence[BaseView]: - """Sequence[:class:`.View`]: A sequence of persistent views added to the client. + """Sequence[Union[:class:`.View`, :class:`.LayoutView`]]: A sequence of persistent views added to the client. .. versionadded:: 2.0 """ diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index d62807779051..c2c40ada5980 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1734,12 +1734,14 @@ async def send( Controls the mentions being processed in this message. .. versionadded:: 1.4 - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] The view to send with the message. If the webhook is partial or is not managed by the library, then you can only send URL buttons. Otherwise, you can send views with any type of components. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. thread: :class:`~discord.abc.Snowflake` The thread to send this webhook to. diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index db59b4659866..90459776102f 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -996,13 +996,15 @@ def send( When sending a Poll via webhook, you cannot manually end it. .. versionadded:: 2.4 - view: :class:`~discord.ui.View` + view: Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`] The view to send with the message. This can only have URL buttons, which donnot require a state to be attached to it. If you want to send a view with any component attached to it, check :meth:`Webhook.send`. .. versionadded:: 2.5 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. Raises -------- From b0bab6d50d449f1ac11d79379137884b5ffcf0e6 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:42:32 +0200 Subject: [PATCH 293/354] fix: `children` parameter being ignored on Container --- discord/ui/container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/ui/container.py b/discord/ui/container.py index c10d24119e72..96bafde0d020 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -104,6 +104,9 @@ def __init__( if children is not MISSING: if len(children) + len(self._children) > 10: raise ValueError('maximum number of children exceeded') + for child in children: + self.add_item(child) + self.spoiler: bool = spoiler self._colour = accent_colour or accent_color From 412caa6c2e26cf3a6213ab283a8bf48e2bd816b1 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:54:06 +0200 Subject: [PATCH 294/354] update ActionRow.select docstring --- discord/ui/action_row.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 70384cf9a0b7..9b01cd3a0ab5 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -411,7 +411,7 @@ def select( """A decorator that attaches a select menu to a component. The function being decorated should have three parameters, ``self`` representing - the :class:`discord.ui.View`, the :class:`discord.Interaction` you receive and + the :class:`discord.ui.LayoutView`, the :class:`discord.Interaction` you receive and the chosen select class. To obtain the selected values inside the callback, you can use the ``values`` attribute of the chosen class in the callback. The list of values From 9026bcbb1d45ba41df09ce9fddff231268fd4987 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:57:02 +0200 Subject: [PATCH 295/354] add note about Item.custom_id --- discord/ui/item.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/ui/item.py b/discord/ui/item.py index 614859d726d2..d735641db181 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -83,6 +83,8 @@ def __init__(self): self._max_row: int = 5 if not self._is_v2() else 10 if self._is_v2(): + # this is done so v2 components can be stored on ViewStore._views + # and does not break v1 components custom_id property self.custom_id: str = os.urandom(16).hex() def to_component_dict(self) -> Dict[str, Any]: From cf949c689fbfd360c4e99984f10562c71f1940f5 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:57:06 +0200 Subject: [PATCH 296/354] chore: some bunch fixes and make interaction_check's work on every item --- discord/ui/action_row.py | 26 +++++++++++++------------- discord/ui/button.py | 10 ++++++++-- discord/ui/container.py | 7 ++++--- discord/ui/item.py | 13 ++++++++++++- discord/ui/select.py | 16 ++++++++-------- discord/ui/view.py | 14 +++++++------- 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 9b01cd3a0ab5..9fa8541c7f23 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -75,8 +75,8 @@ class _ActionRowCallback: __slots__ = ('row', 'callback', 'item') - def __init__(self, callback: ItemCallbackType[Any, Any], row: ActionRow, item: Item[Any]) -> None: - self.callback: ItemCallbackType[Any, Any] = callback + def __init__(self, callback: ItemCallbackType[Any], row: ActionRow, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any] = callback self.row: ActionRow = row self.item: Item[Any] = item @@ -97,7 +97,7 @@ class ActionRow(Item[V]): The ID of this component. This must be unique across the view. """ - __action_row_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = [] + __action_row_children_items__: ClassVar[List[ItemCallbackType[Any]]] = [] __discord_ui_action_row__: ClassVar[bool] = True __discord_ui_update_view__: ClassVar[bool] = True @@ -110,7 +110,7 @@ def __init__(self, *, id: Optional[int] = None) -> None: def __init_subclass__(cls) -> None: super().__init_subclass__() - children: Dict[str, ItemCallbackType[Any, Any]] = {} + children: Dict[str, ItemCallbackType[Any]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): @@ -269,7 +269,7 @@ def button( disabled: bool = False, style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, - ) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: + ) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]: """A decorator that attaches a button to a component. The function being decorated should have three parameters, ``self`` representing @@ -302,7 +302,7 @@ def button( or a full :class:`.Emoji`. """ - def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: + def decorator(func: ItemCallbackType[Button[V]]) -> ItemCallbackType[Button[V]]: ret = _button( label=label, custom_id=custom_id, @@ -328,7 +328,7 @@ def select( min_values: int = ..., max_values: int = ..., disabled: bool = ..., - ) -> SelectCallbackDecorator[V, SelectT]: + ) -> SelectCallbackDecorator[SelectT]: ... @overload @@ -344,7 +344,7 @@ def select( max_values: int = ..., disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., - ) -> SelectCallbackDecorator[V, UserSelectT]: + ) -> SelectCallbackDecorator[UserSelectT]: ... @overload @@ -360,7 +360,7 @@ def select( max_values: int = ..., disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., - ) -> SelectCallbackDecorator[V, RoleSelectT]: + ) -> SelectCallbackDecorator[RoleSelectT]: ... @overload @@ -376,7 +376,7 @@ def select( max_values: int = ..., disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., - ) -> SelectCallbackDecorator[V, ChannelSelectT]: + ) -> SelectCallbackDecorator[ChannelSelectT]: ... @overload @@ -392,7 +392,7 @@ def select( max_values: int = ..., disabled: bool = ..., default_values: Sequence[ValidDefaultValues] = ..., - ) -> SelectCallbackDecorator[V, MentionableSelectT]: + ) -> SelectCallbackDecorator[MentionableSelectT]: ... def select( @@ -407,7 +407,7 @@ def select( max_values: int = 1, disabled: bool = False, default_values: Sequence[ValidDefaultValues] = MISSING, - ) -> SelectCallbackDecorator[V, BaseSelectT]: + ) -> SelectCallbackDecorator[BaseSelectT]: """A decorator that attaches a select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -477,7 +477,7 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe Number of items must be in range of ``min_values`` and ``max_values``. """ - def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: + def decorator(func: ItemCallbackType[BaseSelectT]) -> ItemCallbackType[BaseSelectT]: r = _select( # type: ignore cls=cls, # type: ignore placeholder=placeholder, diff --git a/discord/ui/button.py b/discord/ui/button.py index 7a60333db414..46230d480d54 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -281,7 +281,8 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[V, Button[V]]], Button[V]]: + id: Optional[int] = None, +) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]: """A decorator that attaches a button to a component. The function being decorated should have three parameters, ``self`` representing @@ -318,9 +319,13 @@ def button( like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). + id: Optional[:class:`int`] + The ID of this component. This must be unique across the view. + + .. versionadded:: 2.6 """ - def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Button[V]]: + def decorator(func: ItemCallbackType[Button[V]]) -> ItemCallbackType[Button[V]]: if not inspect.iscoroutinefunction(func): raise TypeError('button function must be a coroutine function') @@ -334,6 +339,7 @@ def decorator(func: ItemCallbackType[V, Button[V]]) -> ItemCallbackType[V, Butto 'emoji': emoji, 'row': row, 'sku_id': None, + 'id': id, } return func diff --git a/discord/ui/container.py b/discord/ui/container.py index 96bafde0d020..58176d0f5326 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -46,8 +46,8 @@ class _ContainerCallback: __slots__ = ('container', 'callback', 'item') - def __init__(self, callback: ItemCallbackType[Any, Any], container: Container, item: Item[Any]) -> None: - self.callback: ItemCallbackType[Any, Any] = callback + def __init__(self, callback: ItemCallbackType[Any], container: Container, item: Item[Any]) -> None: + self.callback: ItemCallbackType[Any] = callback self.container: Container = container self.item: Item[Any] = item @@ -63,7 +63,7 @@ class Container(Item[V]): Parameters ---------- children: List[:class:`Item`] - The initial children or :class:`View` s of this container. Can have up to 10 + The initial children of this container. Can have up to 10 items. accent_colour: Optional[:class:`.Colour`] The colour of the container. Defaults to ``None``. @@ -124,6 +124,7 @@ def _init_children(self) -> List[Item[Any]]: if getattr(raw, '__discord_ui_section__', False) and raw.accessory.is_dispatchable(): # type: ignore self.__dispatchable.append(raw.accessory) # type: ignore elif getattr(raw, '__discord_ui_action_row__', False) and raw.is_dispatchable(): + raw._parent = self # type: ignore self.__dispatchable.extend(raw._children) # type: ignore else: # action rows can be created inside containers, and then callbacks can exist here diff --git a/discord/ui/item.py b/discord/ui/item.py index d735641db181..4206274c34dd 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -43,7 +43,7 @@ I = TypeVar('I', bound='Item[Any]') V = TypeVar('V', bound='BaseView', covariant=True) -ItemCallbackType = Callable[[V, Interaction[Any], I], Coroutine[Any, Any, Any]] +ItemCallbackType = Callable[[Any, Interaction[Any], I], Coroutine[Any, Any, Any]] class Item(Generic[V]): @@ -151,6 +151,17 @@ def id(self) -> Optional[int]: def id(self, value: Optional[int]) -> None: self._id = value + async def _run_checks(self, interaction: Interaction[ClientT]) -> bool: + can_run = await self.interaction_check(interaction) + + if can_run: + parent = getattr(self, '_parent', None) + + if parent is not None: + can_run = await parent._run_checks(interaction) + + return can_run + async def callback(self, interaction: Interaction[ClientT]) -> Any: """|coro| diff --git a/discord/ui/select.py b/discord/ui/select.py index e2d3d34d2583..40b8a26f38dc 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -109,7 +109,7 @@ RoleSelectT = TypeVar('RoleSelectT', bound='RoleSelect[Any]') ChannelSelectT = TypeVar('ChannelSelectT', bound='ChannelSelect[Any]') MentionableSelectT = TypeVar('MentionableSelectT', bound='MentionableSelect[Any]') -SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[V, BaseSelectT]], BaseSelectT] +SelectCallbackDecorator: TypeAlias = Callable[[ItemCallbackType[BaseSelectT]], BaseSelectT] DefaultSelectComponentTypes = Literal[ ComponentType.user_select, ComponentType.role_select, @@ -936,7 +936,7 @@ def select( disabled: bool = ..., row: Optional[int] = ..., id: Optional[int] = ..., -) -> SelectCallbackDecorator[V, SelectT]: +) -> SelectCallbackDecorator[SelectT]: ... @@ -954,7 +954,7 @@ def select( default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., id: Optional[int] = ..., -) -> SelectCallbackDecorator[V, UserSelectT]: +) -> SelectCallbackDecorator[UserSelectT]: ... @@ -972,7 +972,7 @@ def select( default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., id: Optional[int] = ..., -) -> SelectCallbackDecorator[V, RoleSelectT]: +) -> SelectCallbackDecorator[RoleSelectT]: ... @@ -990,7 +990,7 @@ def select( default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., id: Optional[int] = ..., -) -> SelectCallbackDecorator[V, ChannelSelectT]: +) -> SelectCallbackDecorator[ChannelSelectT]: ... @@ -1008,7 +1008,7 @@ def select( default_values: Sequence[ValidDefaultValues] = ..., row: Optional[int] = ..., id: Optional[int] = ..., -) -> SelectCallbackDecorator[V, MentionableSelectT]: +) -> SelectCallbackDecorator[MentionableSelectT]: ... @@ -1025,7 +1025,7 @@ def select( default_values: Sequence[ValidDefaultValues] = MISSING, row: Optional[int] = None, id: Optional[int] = None, -) -> SelectCallbackDecorator[V, BaseSelectT]: +) -> SelectCallbackDecorator[BaseSelectT]: """A decorator that attaches a select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -1110,7 +1110,7 @@ async def select_channels(self, interaction: discord.Interaction, select: Channe .. versionadded:: 2.6 """ - def decorator(func: ItemCallbackType[V, BaseSelectT]) -> ItemCallbackType[V, BaseSelectT]: + def decorator(func: ItemCallbackType[BaseSelectT]) -> ItemCallbackType[BaseSelectT]: if not inspect.iscoroutinefunction(func): raise TypeError('select function must be a coroutine function') callback_cls = getattr(cls, '__origin__', cls) diff --git a/discord/ui/view.py b/discord/ui/view.py index 5107716bd3b3..0fb57787113c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -84,7 +84,7 @@ from ..state import ConnectionState from .modal import Modal - ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]] + ItemLike = Union[ItemCallbackType[Any], Item[Any]] _log = logging.getLogger(__name__) @@ -185,8 +185,8 @@ def v2_weights(self) -> bool: class _ViewCallback: __slots__ = ('view', 'callback', 'item') - def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None: - self.callback: ItemCallbackType[Any, Any] = callback + def __init__(self, callback: ItemCallbackType[Any], view: BaseView, item: Item[BaseView]) -> None: + self.callback: ItemCallbackType[Any] = callback self.view: BaseView = view self.item: Item[BaseView] = item @@ -452,7 +452,7 @@ async def _scheduled_task(self, item: Item, interaction: Interaction): try: item._refresh_state(interaction, interaction.data) # type: ignore - allow = await item.interaction_check(interaction) and await self.interaction_check(interaction) + allow = await item._run_checks(interaction) and await self.interaction_check(interaction) if not allow: return @@ -581,13 +581,13 @@ class View(BaseView): def __init_subclass__(cls) -> None: warnings.warn( - 'discord.ui.View and subclasses are deprecated and will be removed in' + 'discord.ui.View and subclasses are deprecated and will be removed in ' 'a future version, use discord.ui.LayoutView instead', DeprecationWarning, ) super().__init_subclass__() - children: Dict[str, ItemCallbackType[Any, Any]] = {} + children: Dict[str, ItemCallbackType[Any]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): @@ -716,7 +716,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: def __init_subclass__(cls) -> None: children: Dict[str, Item[Any]] = {} - callback_children: Dict[str, ItemCallbackType[Any, Any]] = {} + callback_children: Dict[str, ItemCallbackType[Any]] = {} row = 0 From fb8e85da7cb681ec15d09456e9dcf2ac2215c2d2 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:59:41 +0200 Subject: [PATCH 297/354] fix: typings --- discord/ui/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 58176d0f5326..d2fdf5e5ea19 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -83,7 +83,7 @@ class Container(Item[V]): The ID of this component. This must be unique across the view. """ - __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any, Any], Item[Any]]]] = [] + __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any], Item[Any]]]] = [] __discord_ui_update_view__: ClassVar[bool] = True __discord_ui_container__: ClassVar[bool] = True @@ -151,7 +151,7 @@ def is_dispatchable(self) -> bool: def __init_subclass__(cls) -> None: super().__init_subclass__() - children: Dict[str, Union[ItemCallbackType[Any, Any], Item[Any]]] = {} + children: Dict[str, Union[ItemCallbackType[Any], Item[Any]]] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if isinstance(member, Item): From fe7d7f2ce6b7942609c04470ff2a5c3b1bb6cbe0 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 12:15:57 +0200 Subject: [PATCH 298/354] chore: Update view param docstring on send methods --- discord/abc.py | 2 +- discord/channel.py | 2 +- discord/ext/commands/context.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 748a021d7113..505552684b68 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1551,7 +1551,7 @@ async def send( .. versionadded:: 2.0 .. versionchanged:: 2.6 - This parameter now accepts :class:`discord.ui.LayoutView` instances. + This now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. diff --git a/discord/channel.py b/discord/channel.py index 8833f566ea13..f17a74ca81d5 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2911,7 +2911,7 @@ async def create_thread( A Discord UI View to add to the message. .. versionchanged:: 2.6 - This parameter now accepts :class:`discord.ui.LayoutView` instances. + This now accepts :class:`discord.ui.LayoutView` instances. stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] A list of stickers to upload. Must be a maximum of 3. suppress_embeds: :class:`bool` diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 20eb7059962d..931142b38872 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -986,10 +986,12 @@ async def send( This is ignored for interaction based contexts. .. versionadded:: 1.6 - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] A Discord UI View to add to the message. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. embeds: List[:class:`~discord.Embed`] A list of embeds to upload. Must be a maximum of 10. From 195b9e75b6680b8698dafb02bb6e306443108ca3 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:32:39 +0200 Subject: [PATCH 299/354] chore: Allow ints on accent_colo(u)r on Container's --- discord/ui/container.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index d2fdf5e5ea19..e45c1dac60e4 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -65,9 +65,9 @@ class Container(Item[V]): children: List[:class:`Item`] The initial children of this container. Can have up to 10 items. - accent_colour: Optional[:class:`.Colour`] + accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]] The colour of the container. Defaults to ``None``. - accent_color: Optional[:class:`.Colour`] + accent_color: Optional[Union[:class:`.Colour`, :class:`int`]] The color of the container. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this container as a spoiler. Defaults @@ -91,8 +91,8 @@ def __init__( self, children: List[Item[V]] = MISSING, *, - accent_colour: Optional[Colour] = None, - accent_color: Optional[Color] = None, + accent_colour: Optional[Union[Colour, int]] = None, + accent_color: Optional[Union[Color, int]] = None, spoiler: bool = False, row: Optional[int] = None, id: Optional[int] = None, @@ -178,12 +178,12 @@ def children(self, value: List[Item[V]]) -> None: self._children = value @property - def accent_colour(self) -> Optional[Colour]: - """Optional[:class:`discord.Colour`]: The colour of the container, or ``None``.""" + def accent_colour(self) -> Optional[Union[Colour, int]]: + """Optional[Union[:class:`discord.Colour`, :class:`int`]]: The colour of the container, or ``None``.""" return self._colour @accent_colour.setter - def accent_colour(self, value: Optional[Colour]) -> None: + def accent_colour(self, value: Optional[Union[Colour, int]]) -> None: self._colour = value accent_color = accent_colour @@ -207,9 +207,14 @@ def to_components(self) -> List[Dict[str, Any]]: def to_component_dict(self) -> Dict[str, Any]: components = self.to_components() + + colour = None + if self._colour: + colour = self._colour if isinstance(self._colour, int) else self._colour.value + base = { 'type': self.type.value, - 'accent_color': self._colour.value if self._colour else None, + 'accent_color': colour, 'spoiler': self.spoiler, 'components': components, } From 70289119d261e0331d9895536038229a3a882b49 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 22:00:27 +0200 Subject: [PATCH 300/354] fix: Item.view not being correctly set when using 'add_item' methods --- discord/ui/action_row.py | 1 + discord/ui/container.py | 4 +++- discord/ui/section.py | 6 +++--- discord/ui/view.py | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 9fa8541c7f23..b3c9837ad1bb 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -197,6 +197,7 @@ def add_item(self, item: Item[Any]) -> Self: if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') + item._view = self._view self._children.append(item) return self diff --git a/discord/ui/container.py b/discord/ui/container.py index e45c1dac60e4..260577de9180 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -110,7 +110,6 @@ def __init__( self.spoiler: bool = spoiler self._colour = accent_colour or accent_color - self._view: Optional[V] = None self.row = row self.id = id @@ -277,6 +276,9 @@ def add_item(self, item: Item[Any]) -> Self: if getattr(item, '__discord_ui_section__', False): self.__dispatchable.append(item.accessory) # type: ignore + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self.view) # type: ignore + return self def remove_item(self, item: Item[Any]) -> Self: diff --git a/discord/ui/section.py b/discord/ui/section.py index 13d13169cb15..98784d50adf0 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -139,9 +139,9 @@ def add_item(self, item: Union[str, Item[Any]]) -> Self: if not isinstance(item, (Item, str)): raise TypeError(f'expected Item or str not {item.__class__.__name__}') - self._children.append( - item if isinstance(item, Item) else TextDisplay(item), - ) + item = item if isinstance(item, Item) else TextDisplay(item) + item._view = self.view + self._children.append(item) return self def remove_item(self, item: Item[Any]) -> Self: diff --git a/discord/ui/view.py b/discord/ui/view.py index 0fb57787113c..716576ccbbfe 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -342,6 +342,10 @@ def add_item(self, item: Item[Any]) -> Self: raise ValueError('v2 items cannot be added to this view') item._view = self + + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self) # type: ignore + self._children.append(item) return self From 86ec83471b65b64e7f7f17d0239d76facffc4320 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 19 Apr 2025 22:04:21 +0200 Subject: [PATCH 301/354] chore: Update BaseView.__repr__ --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 716576ccbbfe..dc6b581cc7a1 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -214,7 +214,7 @@ def _is_v2(self) -> bool: return False def __repr__(self) -> str: - return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}' + return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' def _init_children(self) -> List[Item[Self]]: children = [] From 8376dbfd496e4f597034cb5d415a8ed6b8ccc535 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:25:23 +0200 Subject: [PATCH 302/354] chore: Add Thumbnail.description char limit to docs --- discord/ui/thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index 7f21edd3aad7..f639269918a4 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -52,7 +52,7 @@ class Thumbnail(Item[V]): to an attachment that matches the ``attachment://filename.extension`` structure. description: Optional[:class:`str`] - The description of this thumbnail. Defaults to ``None``. + The description of this thumbnail. Up to 256 characters. Defaults to ``None``. spoiler: :class:`bool` Whether to flag this thumbnail as a spoiler. Defaults to ``False``. row: Optional[:class:`int`] From 9f3f8f1c38ab0e58722b2a8c47ceb17ec10864c4 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:27:19 +0200 Subject: [PATCH 303/354] chore: Add MediaGalleryItem.description char limit to docs --- discord/components.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/components.py b/discord/components.py index 80842f7fcff1..22c3d714cb54 100644 --- a/discord/components.py +++ b/discord/components.py @@ -993,7 +993,8 @@ class MediaGalleryItem: file uploaded as an attachment in the message, that can be accessed using the ``attachment://file-name.extension`` format. description: Optional[:class:`str`] - The description to show within this item. + The description to show within this item. Up to 256 characters. Defaults + to ``None``. spoiler: :class:`bool` Whether this item should be flagged as a spoiler. """ From e0c07539a98af319b5b5cff321928fec5d991772 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:51:42 +0200 Subject: [PATCH 304/354] chore: Update interactions docs --- discord/interactions.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index e02be7359480..658ec4127887 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -929,8 +929,11 @@ async def send_message( A list of files to upload. Must be a maximum of 10. tts: :class:`bool` Indicates if the message should be sent using text-to-speech. - view: :class:`discord.ui.View` + view: Union[:class:`discord.ui.View`, :class:`discord.ui.LayoutView`] The view to send with the message. + + .. versionchanged:: 2.6 + This now accepts :class:`discord.ui.LayoutView` instances. ephemeral: :class:`bool` Indicates if the message should only be visible to the user who started the interaction. If a view is sent with an ephemeral message and it has no timeout set then the timeout @@ -1076,9 +1079,12 @@ async def edit_message( New files will always appear after current attachments. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. allowed_mentions: Optional[:class:`~discord.AllowedMentions`] Controls the mentions being processed in this message. See :meth:`.Message.edit` for more information. @@ -1363,9 +1369,12 @@ async def edit( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. delete_after: Optional[:class:`float`] If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, From 2248df00a310e0e819d465c6a4b14583a573c015 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:52:05 +0200 Subject: [PATCH 305/354] chore: Add char limit to TextDisplay --- discord/ui/text_display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index 409b68272187..f311cf66c4b3 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -47,7 +47,7 @@ class TextDisplay(Item[V]): Parameters ---------- content: :class:`str` - The content of this text display. + The content of this text display. Up to 4000 characters. row: Optional[:class:`int`] The relative row this text display belongs to. By default items are arranged automatically into those rows. If you'd From 22e473891824c8ea8f92e5a000ed7c651af5a20a Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 14:12:16 +0200 Subject: [PATCH 306/354] chore: Fix interaction_check not being called correctly --- discord/ui/action_row.py | 1 + discord/ui/container.py | 6 ++++++ discord/ui/item.py | 8 +++----- discord/ui/section.py | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index b3c9837ad1bb..b2d713f5a651 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -198,6 +198,7 @@ def add_item(self, item: Item[Any]) -> Self: raise TypeError(f'expected Item not {item.__class__.__name__}') item._view = self._view + item._parent = self self._children.append(item) return self diff --git a/discord/ui/container.py b/discord/ui/container.py index 260577de9180..84147d88228b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -275,10 +275,16 @@ def add_item(self, item: Item[Any]) -> Self: if item.is_dispatchable(): if getattr(item, '__discord_ui_section__', False): self.__dispatchable.append(item.accessory) # type: ignore + elif hasattr(item, '_children'): + self.__dispatchable.extend([i for i in item._children if i.is_dispatchable()]) # type: ignore + else: + self.__dispatchable.append(item) if getattr(item, '__discord_ui_update_view__', False): item._update_children_view(self.view) # type: ignore + item._view = self.view + item._parent = self return self def remove_item(self, item: Item[Any]) -> Self: diff --git a/discord/ui/item.py b/discord/ui/item.py index 4206274c34dd..47a31633bd0b 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -81,6 +81,7 @@ def __init__(self): self._provided_custom_id: bool = False self._id: Optional[int] = None self._max_row: int = 5 if not self._is_v2() else 10 + self._parent: Optional[Item] = None if self._is_v2(): # this is done so v2 components can be stored on ViewStore._views @@ -154,11 +155,8 @@ def id(self, value: Optional[int]) -> None: async def _run_checks(self, interaction: Interaction[ClientT]) -> bool: can_run = await self.interaction_check(interaction) - if can_run: - parent = getattr(self, '_parent', None) - - if parent is not None: - can_run = await parent._run_checks(interaction) + if can_run and self._parent: + can_run = await self._parent._run_checks(interaction) return can_run diff --git a/discord/ui/section.py b/discord/ui/section.py index 98784d50adf0..53d433c3ea77 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -141,6 +141,7 @@ def add_item(self, item: Union[str, Item[Any]]) -> Self: item = item if isinstance(item, Item) else TextDisplay(item) item._view = self.view + item._parent = self self._children.append(item) return self From 92cb5575e31ebf2621a337b400984f120ce9313e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Sun, 20 Apr 2025 14:23:08 +0200 Subject: [PATCH 307/354] chore: Remove leftover code --- discord/http.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/http.py b/discord/http.py index e0fca595861d..4e12de8bd47c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -192,8 +192,6 @@ def handle_message_parameters( if view is not MISSING: if view is not None: - if getattr(view, '__discord_ui_container__', False): - raise TypeError('Containers must be wrapped around Views') payload['components'] = view.to_components() if view.has_components_v2(): From 876397e5ad191a788643ee6a1a555638605a9779 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 22 Apr 2025 18:28:36 +0200 Subject: [PATCH 308/354] chore: Improve Items documentation --- discord/ui/action_row.py | 36 ++++++++++++++++++++++++++++++++++-- discord/ui/container.py | 35 ++++++++++++++++++++++++++++++++++- discord/ui/file.py | 15 +++++++++++++++ discord/ui/view.py | 4 ++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index b2d713f5a651..22df88f85618 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -87,10 +87,42 @@ def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: class ActionRow(Item[V]): """Represents a UI action row. - This object can be inherited. + This is a top-level layout component that can only be used on :class:`LayoutView` + and can contain :class:`Button` 's and :class:`Select` 's in it. + + This can be inherited. + + .. note:: + + Action rows can contain up to 5 components, which is, 5 buttons or 1 select. .. versionadded:: 2.6 + Examples + -------- + + .. code-block:: python3 + + import discord + from discord import ui + + # you can subclass it and add components with the decorators + class MyActionRow(ui.ActionRow): + @ui.button(label='Click Me!') + async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked me!') + + # or use it directly on LayoutView + class MyView(ui.LayoutView): + row = ui.ActionRow() + # or you can use your subclass: + # row = MyActionRow() + + # you can create items with row.button and row.select + @row.button(label='A button!') + async def row_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked a button!') + Parameters ---------- id: Optional[:class:`int`] @@ -127,7 +159,7 @@ def _init_children(self) -> List[Item[Any]]: for func in self.__action_row_children_items__: item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item.callback = _ActionRowCallback(func, self, item) # type: ignore - item._parent = getattr(func, '__discord_ui_parent__', self) # type: ignore + item._parent = getattr(func, '__discord_ui_parent__', self) setattr(self, func.__name__, item) children.append(item) return children diff --git a/discord/ui/container.py b/discord/ui/container.py index 84147d88228b..a90df10b93f0 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -58,8 +58,41 @@ def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]: class Container(Item[V]): """Represents a UI container. + This is a top-level layout component that can only be used on :class:`LayoutView` + and can contain :class:`ActionRow` 's, :class:`TextDisplay` 's, :class:`Section` 's, + :class:`MediaGallery` 's, and :class:`File` 's in it. + + This can be inherited. + + .. note:: + + Containers can contain up to 10 top-level components. + .. versionadded:: 2.6 + Examples + -------- + + .. code-block:: python3 + + import discord + from discord import ui + + # you can subclass it and add components as you would add them + # in a LayoutView + class MyContainer(ui.Container): + action_row = ui.ActionRow() + + @action_row.button(label='A button in a container!') + async def a_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('You clicked a button!') + + # or use it directly on LayoutView + class MyView(ui.LayoutView): + container = ui.Container([ui.TextDisplay('I am a text display on a container!')]) + # or you can use your subclass: + # container = MyContainer() + Parameters ---------- children: List[:class:`Item`] @@ -123,7 +156,7 @@ def _init_children(self) -> List[Item[Any]]: if getattr(raw, '__discord_ui_section__', False) and raw.accessory.is_dispatchable(): # type: ignore self.__dispatchable.append(raw.accessory) # type: ignore elif getattr(raw, '__discord_ui_action_row__', False) and raw.is_dispatchable(): - raw._parent = self # type: ignore + raw._parent = self self.__dispatchable.extend(raw._children) # type: ignore else: # action rows can be created inside containers, and then callbacks can exist here diff --git a/discord/ui/file.py b/discord/ui/file.py index 0f6875421521..2e10ff98999d 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -42,8 +42,23 @@ class File(Item[V]): """Represents a UI file component. + This is a top-level layout component that can only be used on :class:`LayoutView`. + .. versionadded:: 2.6 + Example + ------- + + .. code-block:: python3 + + import discord + from discord import ui + + class MyView(ui.LayoutView): + file = ui.File('attachment://file.txt') + # attachment://file.txt points to an attachment uploaded alongside + # this view + Parameters ---------- media: Union[:class:`str`, :class:`.UnfurledMediaItem`] diff --git a/discord/ui/view.py b/discord/ui/view.py index dc6b581cc7a1..61abd98757ef 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -706,6 +706,10 @@ def clear_items(self) -> Self: class LayoutView(BaseView): """Represents a layout view for components. + This object must be inherited to create a UI within Discord. + + + .. versionadded:: 2.6 Parameters From 4cb3b410a7f61d080e84823274a5a489abd3cf68 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:08:29 +0200 Subject: [PATCH 309/354] chore: more docs things ig --- discord/ui/file.py | 3 +-- discord/ui/media_gallery.py | 4 +++- discord/ui/section.py | 2 ++ discord/ui/separator.py | 2 ++ discord/ui/text_display.py | 2 ++ 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/discord/ui/file.py b/discord/ui/file.py index 2e10ff98999d..341860cc739d 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -56,8 +56,7 @@ class File(Item[V]): class MyView(ui.LayoutView): file = ui.File('attachment://file.txt') - # attachment://file.txt points to an attachment uploaded alongside - # this view + # attachment://file.txt points to an attachment uploaded alongside this view Parameters ---------- diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index 3deca63c86cf..e3db92215470 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -45,7 +45,9 @@ class MediaGallery(Item[V]): """Represents a UI media gallery. - This can contain up to 10 :class:`.MediaGalleryItem` s. + Can contain up to 10 :class:`.MediaGalleryItem` 's. + + This is a top-level layout component that can only be used on :class:`LayoutView`. .. versionadded:: 2.6 diff --git a/discord/ui/section.py b/discord/ui/section.py index 53d433c3ea77..11de2ec18dc6 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -44,6 +44,8 @@ class Section(Item[V]): """Represents a UI section. + This is a top-level layout component that can only be used on :class:`LayoutView` + .. versionadded:: 2.6 Parameters diff --git a/discord/ui/separator.py b/discord/ui/separator.py index e212f4b4e4e5..48908df9d6eb 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -42,6 +42,8 @@ class Separator(Item[V]): """Represents a UI separator. + This is a top-level layout component that can only be used on :class:`LayoutView`. + .. versionadded:: 2.6 Parameters diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index f311cf66c4b3..beff74c6eac3 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -42,6 +42,8 @@ class TextDisplay(Item[V]): """Represents a UI text display. + This is a top-level layout component that can only be used on :class:`LayoutView`. + .. versionadded:: 2.6 Parameters From f5ec966a7b00caf552f10e1524cbfacf4eb19ece Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:53:06 +0200 Subject: [PATCH 310/354] chore: typings and docs and idk what more --- discord/abc.py | 42 ++++++++++-- discord/channel.py | 42 +++++++++++- discord/ext/commands/context.py | 52 ++++++++++++-- discord/interactions.py | 118 +++++++++++++++++++++++++++++++- discord/message.py | 64 ++++++++++++++--- discord/ui/view.py | 2 +- discord/webhook/async_.py | 72 +++++++++++++++++-- discord/webhook/sync.py | 44 +++++++++++- 8 files changed, 405 insertions(+), 31 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 505552684b68..5ea20b558a70 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -95,7 +95,7 @@ ) from .poll import Poll from .threads import Thread - from .ui.view import BaseView + from .ui.view import BaseView, View, LayoutView from .types.channel import ( PermissionOverwrite as PermissionOverwritePayload, Channel as ChannelPayload, @@ -1374,6 +1374,38 @@ class Messageable: async def _get_channel(self) -> MessageableChannel: raise NotImplementedError + @overload + async def send( + self, + *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def send( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def send( self, @@ -1388,7 +1420,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1409,7 +1441,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1430,7 +1462,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1451,7 +1483,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., diff --git a/discord/channel.py b/discord/channel.py index f17a74ca81d5..314c46faec4c 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -100,7 +100,7 @@ from .file import File from .user import ClientUser, User, BaseUser from .guild import Guild, GuildChannel as GuildChannelType - from .ui.view import BaseView + from .ui.view import BaseView, View, LayoutView from .types.channel import ( TextChannel as TextChannelPayload, NewsChannel as NewsChannelPayload, @@ -2841,6 +2841,46 @@ async def create_tag( return result + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = MISSING, + slowmode_delay: Optional[int] = None, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + mention_author: bool = MISSING, + view: LayoutView, + suppress_embeds: bool = False, + reason: Optional[str] = None, + ) -> ThreadWithMessage: + ... + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: ThreadArchiveDuration = MISSING, + slowmode_delay: Optional[int] = None, + content: Optional[str] = None, + tts: bool = False, + embed: Embed = MISSING, + embeds: Sequence[Embed] = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + mention_author: bool = MISSING, + applied_tags: Sequence[ForumTag] = MISSING, + view: View = MISSING, + suppress_embeds: bool = False, + reason: Optional[str] = None, + ) -> ThreadWithMessage: + ... + async def create_thread( self, *, diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 931142b38872..1e957feb4f16 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -48,7 +48,7 @@ from discord.mentions import AllowedMentions from discord.sticker import GuildSticker, StickerItem from discord.message import MessageReference, PartialMessage - from discord.ui.view import BaseView + from discord.ui.view import BaseView, View, LayoutView from discord.types.interactions import ApplicationCommandInteractionData from discord.poll import Poll @@ -628,6 +628,40 @@ async def send_help(self, *args: Any) -> Any: except CommandError as e: await cmd.on_help_command_error(self, e) + @overload + async def reply( + self, + *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def reply( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def reply( self, @@ -642,7 +676,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -664,7 +698,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -686,7 +720,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -708,7 +742,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -817,6 +851,14 @@ async def defer(self, *, ephemeral: bool = False) -> None: if self.interaction: await self.interaction.response.defer(ephemeral=ephemeral) + @overload + async def send( + self, + *, + view: LayoutView, + ) -> Message: + ... + @overload async def send( self, diff --git a/discord/interactions.py b/discord/interactions.py index 658ec4127887..7b0b9c493787 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -27,7 +27,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List +from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union, List, overload import asyncio import datetime @@ -76,7 +76,7 @@ from .mentions import AllowedMentions from aiohttp import ClientSession from .embeds import Embed - from .ui.view import BaseView + from .ui.view import BaseView, View, LayoutView from .app_commands.models import Choice, ChoiceT from .ui.modal import Modal from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel @@ -469,6 +469,30 @@ async def original_response(self) -> InteractionMessage: self._original_response = message return message + @overload + async def edit_original_response( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = None, + ) -> InteractionMessage: + ... + + @overload + async def edit_original_response( + self, + *, + content: Optional[str] = MISSING, + embeds: Sequence[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + poll: Poll = MISSING, + ) -> InteractionMessage: + ... + async def edit_original_response( self, *, @@ -889,6 +913,41 @@ async def pong(self) -> None: ) self._response_type = InteractionResponseType.pong + @overload + async def send_message( + self, + *, + file: File = MISSING, + files: Sequence[File] = MISSING, + view: LayoutView, + ephemeral: bool = False, + allowed_mentions: AllowedMentions = MISSING, + suppress_embeds: bool = False, + silent: bool = False, + delete_after: Optional[float] = None, + ) -> InteractionCallbackResponse[ClientT]: + ... + + @overload + async def send_message( + self, + content: Optional[Any] = None, + *, + embed: Embed = MISSING, + embeds: Sequence[Embed] = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + view: View = MISSING, + tts: bool = False, + ephemeral: bool = False, + allowed_mentions: AllowedMentions = MISSING, + suppress_embeds: bool = False, + silent: bool = False, + delete_after: Optional[float] = None, + poll: Poll = MISSING, + ) -> InteractionCallbackResponse[ClientT]: + ... + async def send_message( self, content: Optional[Any] = None, @@ -1042,6 +1101,33 @@ async def inner_call(delay: float = delete_after): type=self._response_type, ) + @overload + async def edit_message( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = MISSING, + delete_after: Optional[float] = None, + suppress_embeds: bool = MISSING, + ) -> Optional[InteractionCallbackResponse[ClientT]]: + ... + + @overload + async def edit_message( + self, + *, + content: Optional[Any] = MISSING, + embed: Optional[Embed] = MISSING, + embeds: Sequence[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = MISSING, + delete_after: Optional[float] = None, + suppress_embeds: bool = MISSING, + ) -> Optional[InteractionCallbackResponse[ClientT]]: + ... + async def edit_message( self, *, @@ -1333,6 +1419,32 @@ class InteractionMessage(Message): __slots__ = () _state: _InteractionMessageState + @overload + async def edit( + self, + *, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = None, + delete_after: Optional[float] = None, + ) -> InteractionMessage: + ... + + @overload + async def edit( + self, + *, + content: Optional[str] = MISSING, + embeds: Sequence[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + delete_after: Optional[float] = None, + poll: Poll = MISSING, + ) -> InteractionMessage: + ... + async def edit( self, *, @@ -1412,7 +1524,7 @@ async def edit( embeds=embeds, embed=embed, attachments=attachments, - view=view, + view=view, # type: ignore allowed_mentions=allowed_mentions, poll=poll, ) diff --git a/discord/message.py b/discord/message.py index e4f19a4dd6bd..f3d364ec81ea 100644 --- a/discord/message.py +++ b/discord/message.py @@ -101,7 +101,7 @@ from .mentions import AllowedMentions from .user import User from .role import Role - from .ui.view import BaseView + from .ui.view import BaseView, View, LayoutView EmojiInputType = Union[Emoji, PartialEmoji, str] @@ -534,7 +534,7 @@ def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): for component_data in data.get('components', []): component = _component_factory(component_data, state) # type: ignore if component is not None: - self.components.append(component) # type: ignore + self.components.append(component) self._state: ConnectionState = state @@ -1302,6 +1302,17 @@ async def delete(delay: float): else: await self._state.http.delete_message(self.channel.id, self.id) + @overload + async def edit( + self, + *, + view: LayoutView, + attachments: Sequence[Union[Attachment, File]] = ..., + delete_after: Optional[float] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + ) -> Message: + ... + @overload async def edit( self, @@ -1311,7 +1322,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[BaseView] = ..., + view: Optional[View] = ..., ) -> Message: ... @@ -1324,7 +1335,7 @@ async def edit( attachments: Sequence[Union[Attachment, File]] = ..., delete_after: Optional[float] = ..., allowed_mentions: Optional[AllowedMentions] = ..., - view: Optional[BaseView] = ..., + view: Optional[View] = ..., ) -> Message: ... @@ -1387,10 +1398,13 @@ async def edit( are used instead. .. versionadded:: 1.4 - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. + Raises ------- HTTPException @@ -1752,6 +1766,38 @@ async def fetch_thread(self) -> Thread: return await self.guild.fetch_channel(self.id) # type: ignore # Can only be Thread in this case + @overload + async def reply( + self, + *, + file: File = ...,g + view: LayoutView, + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def reply( + self, + *, + files: Sequence[File] = ..., + view: LayoutView, + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: + ... + @overload async def reply( self, @@ -1766,7 +1812,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1787,7 +1833,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1808,7 +1854,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., @@ -1829,7 +1875,7 @@ async def reply( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., silent: bool = ..., poll: Poll = ..., diff --git a/discord/ui/view.py b/discord/ui/view.py index 61abd98757ef..e1f53bd8eb8d 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -708,7 +708,7 @@ class LayoutView(BaseView): This object must be inherited to create a UI within Discord. - + .. versionadded:: 2.6 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index c2c40ada5980..897ddadd377c 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -71,7 +71,7 @@ from ..emoji import Emoji from ..channel import VoiceChannel from ..abc import Snowflake - from ..ui.view import BaseView + from ..ui.view import BaseView, View, LayoutView from ..poll import Poll import datetime from ..types.webhook import ( @@ -1605,6 +1605,44 @@ def _create_message(self, data, *, thread: Snowflake): # state is artificial return WebhookMessage(data=data, state=state, channel=channel) # type: ignore + @overload + async def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[True], + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> WebhookMessage: + ... + + @overload + async def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[False] = ..., + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> None: + ... + @overload async def send( self, @@ -1619,7 +1657,7 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: BaseView = MISSING, + view: View = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: Literal[True], @@ -1644,7 +1682,7 @@ async def send( embed: Embed = MISSING, embeds: Sequence[Embed] = MISSING, allowed_mentions: AllowedMentions = MISSING, - view: BaseView = MISSING, + view: View = MISSING, thread: Snowflake = MISSING, thread_name: str = MISSING, wait: Literal[False] = ..., @@ -1940,6 +1978,30 @@ async def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> Web ) return self._create_message(data, thread=thread) + @overload + async def edit_message( + self, + message_id: int, + *, + view: LayoutView, + ) -> WebhookMessage: + ... + + @overload + async def edit_message( + self, + message_id: int, + *, + content: Optional[str] = MISSING, + embeds: Sequence[Embed] = MISSING, + embed: Optional[Embed] = MISSING, + attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[View] = MISSING, + allowed_mentions: Optional[AllowedMentions] = None, + thread: Snowflake = MISSING, + ) -> WebhookMessage: + ... + async def edit_message( self, message_id: int, @@ -1987,12 +2049,14 @@ async def edit_message( allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. - view: Optional[:class:`~discord.ui.View`] + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] The updated view to update this message with. If ``None`` is passed then the view is removed. The webhook must have state attached, similar to :meth:`send`. .. versionadded:: 2.0 + .. versionchanged:: 2.6 + This now accepts :class:`~discord.ui.LayoutView` instances. thread: :class:`~discord.abc.Snowflake` The thread the webhook message belongs to. diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 90459776102f..9c211898d812 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -66,7 +66,7 @@ from ..message import Attachment from ..abc import Snowflake from ..state import ConnectionState - from ..ui.view import BaseView + from ..ui.view import BaseView, View, LayoutView from ..types.webhook import ( Webhook as WebhookPayload, ) @@ -856,6 +856,44 @@ def _create_message(self, data: MessagePayload, *, thread: Snowflake = MISSING) # state is artificial return SyncWebhookMessage(data=data, state=state, channel=channel) # type: ignore + @overload + def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[True], + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> SyncWebhookMessage: + ... + + @overload + def send( + self, + *, + username: str = MISSING, + avatar_url: Any = MISSING, + file: File = MISSING, + files: Sequence[File] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: LayoutView, + wait: Literal[False] = ..., + thread: Snowflake = MISSING, + thread_name: str = MISSING, + suppress_embeds: bool = MISSING, + silent: bool = MISSING, + applied_tags: List[ForumTag] = MISSING, + ) -> None: + ... + @overload def send( self, @@ -876,7 +914,7 @@ def send( silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, - view: BaseView = MISSING, + view: View = MISSING, ) -> SyncWebhookMessage: ... @@ -900,7 +938,7 @@ def send( silent: bool = MISSING, applied_tags: List[ForumTag] = MISSING, poll: Poll = MISSING, - view: BaseView = MISSING, + view: View = MISSING, ) -> None: ... From 0dbd46529ac83a9d81e97312320000e6b9dfeb53 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:53:19 +0200 Subject: [PATCH 311/354] fix: g --- discord/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index f3d364ec81ea..f6fba87cba59 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1770,7 +1770,7 @@ async def fetch_thread(self) -> Thread: async def reply( self, *, - file: File = ...,g + file: File = ..., view: LayoutView, delete_after: float = ..., nonce: Union[str, int] = ..., From af952d3066c04c8017f7f718bca5225e87372285 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:55:34 +0200 Subject: [PATCH 312/354] chore: add LayoutView example --- examples/views/layout.py | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 examples/views/layout.py diff --git a/examples/views/layout.py b/examples/views/layout.py new file mode 100644 index 000000000000..08d43c1af3e2 --- /dev/null +++ b/examples/views/layout.py @@ -0,0 +1,49 @@ +# This example requires the 'message_content' privileged intent to function. + +from discord.ext import commands + +import discord + + +class Bot(commands.Bot): + def __init__(self): + intents = discord.Intents.default() + intents.message_content = True + + super().__init__(command_prefix=commands.when_mentioned_or('$'), intents=intents) + + async def on_ready(self): + print(f'Logged in as {self.user} (ID: {self.user.id})') + print('------') + + +# Define a LayoutView, which will allow us to add v2 components to it. +class Layout(discord.ui.LayoutView): + # you can add any top-level component (ui.ActionRow, ui.Section, ui.Container, ui.File, etc.) here + + action_row = discord.ui.ActionRow() + + @action_row.button(label='Click Me!') + async def action_row_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message('Hi!', ephemeral=True) + + container = discord.ui.Container( + [ + discord.ui.TextDisplay( + 'Click the above button to receive a **very special** message!', + ), + ], + accent_colour=discord.Colour.blurple(), + ) + + +bot = Bot() + + +@bot.command() +async def layout(ctx: commands.Context): + """Sends a very special message!""" + await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any other content + + +bot.run('token') From f5415f5c59191bb2fdf2e6764286cfeca573630b Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 23 Apr 2025 02:00:14 +0200 Subject: [PATCH 313/354] chore: remove deprecation warning --- discord/ui/view.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index e1f53bd8eb8d..32dbfa167458 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -571,8 +571,6 @@ class View(BaseView): This object must be inherited to create a UI within Discord. .. versionadded:: 2.0 - .. deprecated:: 2.6 - This class is deprecated and will be removed in a future version. Use :class:`LayoutView` instead. Parameters ----------- @@ -584,11 +582,6 @@ class View(BaseView): __discord_ui_view__: ClassVar[bool] = True def __init_subclass__(cls) -> None: - warnings.warn( - 'discord.ui.View and subclasses are deprecated and will be removed in ' - 'a future version, use discord.ui.LayoutView instead', - DeprecationWarning, - ) super().__init_subclass__() children: Dict[str, ItemCallbackType[Any]] = {} From 952a623d232b90e53b35d12050ae999cd8143422 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 23 Apr 2025 02:05:49 +0200 Subject: [PATCH 314/354] remove unused import --- discord/ui/view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 32dbfa167458..4fd528ee3e72 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -24,7 +24,6 @@ from __future__ import annotations -import warnings from typing import ( Any, Callable, From c5d7450d86d5a6f6d2dafd80e056a57f351836c7 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 23 Apr 2025 19:58:21 +0200 Subject: [PATCH 315/354] fix: strange error: https://discord.com/channels/336642139381301249/1345167602304946206/1364416832584421486 --- discord/ui/container.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index a90df10b93f0..4c8b254e13e6 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -23,6 +23,7 @@ """ from __future__ import annotations +import copy from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union from .item import Item, ItemCallbackType @@ -116,7 +117,7 @@ class MyView(ui.LayoutView): The ID of this component. This must be unique across the view. """ - __container_children_items__: ClassVar[List[Union[ItemCallbackType[Any], Item[Any]]]] = [] + __container_children_items__: ClassVar[Dict[str, Union[ItemCallbackType[Any], Item[Any]]]] = {} __discord_ui_update_view__: ClassVar[bool] = True __discord_ui_container__: ClassVar[bool] = True @@ -148,16 +149,28 @@ def __init__( def _init_children(self) -> List[Item[Any]]: children = [] + parents = {} - for raw in self.__container_children_items__: + for name, raw in self.__container_children_items__.items(): if isinstance(raw, Item): - children.append(raw) + if getattr(raw, '__discord_ui_action_row__', False): + item = copy.deepcopy(raw) + # we need to deepcopy this object and set it later to prevent + # errors reported on the bikeshedding post + item._parent = self - if getattr(raw, '__discord_ui_section__', False) and raw.accessory.is_dispatchable(): # type: ignore - self.__dispatchable.append(raw.accessory) # type: ignore - elif getattr(raw, '__discord_ui_action_row__', False) and raw.is_dispatchable(): - raw._parent = self - self.__dispatchable.extend(raw._children) # type: ignore + if item.is_dispatchable(): + self.__dispatchable.extend(item._children) # type: ignore + else: + item = raw + + if getattr(item, '__discord_ui_section__', False) and item.accessory.is_dispatchable(): # type: ignore + self.__dispatchable.append(item.accessory) # type: ignore + + setattr(self, name, item) + children.append(item) + + parents[raw] = item else: # action rows can be created inside containers, and then callbacks can exist here # so we create items based off them @@ -170,7 +183,7 @@ def _init_children(self) -> List[Item[Any]]: parent = getattr(raw, '__discord_ui_parent__', None) if parent is None: raise RuntimeError(f'{raw.__name__} is not a valid item for a Container') - parent._children.append(item) + parents.get(parent, parent)._children.append(item) # we donnot append it to the children list because technically these buttons and # selects are not from the container but the action row itself. self.__dispatchable.append(item) @@ -189,9 +202,9 @@ def __init_subclass__(cls) -> None: if isinstance(member, Item): children[name] = member if hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): - children[name] = member + children[name] = copy.copy(member) - cls.__container_children_items__ = list(children.values()) + cls.__container_children_items__ = children def _update_children_view(self, view) -> None: for child in self._children: From dbd8cd6cd33f3e17ef961d275a14b75609e8a6a0 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:33:37 +0200 Subject: [PATCH 316/354] chore: update container and things --- discord/ui/container.py | 9 ++++++++- discord/ui/section.py | 9 ++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 4c8b254e13e6..c9222262b4d3 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -24,6 +24,7 @@ from __future__ import annotations import copy +import os from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union from .item import Item, ItemCallbackType @@ -161,8 +162,14 @@ def _init_children(self) -> List[Item[Any]]: if item.is_dispatchable(): self.__dispatchable.extend(item._children) # type: ignore + if getattr(raw, '__discord_ui_section__', False): + item = copy.copy(raw) + if item.accessory.is_dispatchable(): # type: ignore + item.accessory = copy.deepcopy(item.accessory) # type: ignore + if item.accessory._provided_custom_id is False: # type: ignore + item.accessory.custom_id = os.urandom(16).hex() # type: ignore else: - item = raw + item = copy.copy(raw) if getattr(item, '__discord_ui_section__', False) and item.accessory.is_dispatchable(): # type: ignore self.__dispatchable.append(item.accessory) # type: ignore diff --git a/discord/ui/section.py b/discord/ui/section.py index 11de2ec18dc6..5d922a51a3b3 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -75,22 +75,21 @@ class Section(Item[V]): def __init__( self, - children: List[Union[Item[Any], str]] = MISSING, + children: List[Union[Item[V], str]] = MISSING, *, - accessory: Item[Any], + accessory: Item[V], row: Optional[int] = None, id: Optional[int] = None, ) -> None: super().__init__() - self._children: List[Item[Any]] = [] + self._children: List[Item[V]] = [] if children is not MISSING: if len(children) > 3: raise ValueError('maximum number of children exceeded') self._children.extend( [c if isinstance(c, Item) else TextDisplay(c) for c in children], ) - self.accessory: Item[Any] = accessory - + self.accessory: Item[V] = accessory self.row = row self.id = id From 7ed69ec7e514c9ecdc4dbbe7293b7750fa281038 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:52:01 +0200 Subject: [PATCH 317/354] chore: children, * -> *children --- discord/ui/container.py | 3 +-- discord/ui/media_gallery.py | 5 ++--- discord/ui/section.py | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index c9222262b4d3..a804d1a91af2 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -124,8 +124,7 @@ class MyView(ui.LayoutView): def __init__( self, - children: List[Item[V]] = MISSING, - *, + *children: Item[V], accent_colour: Optional[Union[Colour, int]] = None, accent_color: Optional[Union[Color, int]] = None, spoiler: bool = False, diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index e3db92215470..b86bbd0b4e4d 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -68,15 +68,14 @@ class MediaGallery(Item[V]): def __init__( self, - items: List[MediaGalleryItem], - *, + *items: MediaGalleryItem, row: Optional[int] = None, id: Optional[int] = None, ) -> None: super().__init__() self._underlying = MediaGalleryComponent._raw_construct( - items=items, + items=list(items), id=id, ) diff --git a/discord/ui/section.py b/discord/ui/section.py index 5d922a51a3b3..ca4f1f2aa347 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -75,8 +75,7 @@ class Section(Item[V]): def __init__( self, - children: List[Union[Item[V], str]] = MISSING, - *, + *children: Union[Item[V], str], accessory: Item[V], row: Optional[int] = None, id: Optional[int] = None, From 95a22ced02815ea97b335fe7054e57db7858e2e0 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:43:46 +0200 Subject: [PATCH 318/354] . --- discord/ui/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index a804d1a91af2..617450773a4b 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -291,7 +291,7 @@ def _update_store_data( @classmethod def from_component(cls, component: ContainerComponent) -> Self: return cls( - children=[_component_to_item(c) for c in component.children], + *[_component_to_item(c) for c in component.children], accent_colour=component.accent_colour, spoiler=component.spoiler, id=component.id, From 776d5e173a4ba76fdc626397a7c99deaa31e7d70 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:44:10 +0200 Subject: [PATCH 319/354] unpack --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index ca4f1f2aa347..355beebc9161 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -197,7 +197,7 @@ def from_component(cls, component: SectionComponent) -> Self: from .view import _component_to_item # >circular import< return cls( - children=[_component_to_item(c) for c in component.components], + *[_component_to_item(c) for c in component.components], accessory=_component_to_item(component.accessory), id=component.id, ) From 038ca4a09c76c7c35118b536d5c44f2098e642b4 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:44:38 +0200 Subject: [PATCH 320/354] more unpack --- discord/ui/media_gallery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index b86bbd0b4e4d..badd495e0802 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -184,6 +184,6 @@ def width(self): @classmethod def from_component(cls, component: MediaGalleryComponent) -> Self: return cls( - items=component.items, + *component.items, id=component.id, ) From ab497987ac36d0928252b3766a18c60dff45a66c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:38:27 +0200 Subject: [PATCH 321/354] chore: Update examples and things --- discord/ui/view.py | 2 +- examples/views/layout.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 4fd528ee3e72..a131414327e3 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -700,7 +700,7 @@ class LayoutView(BaseView): This object must be inherited to create a UI within Discord. - + You can find usage examples in the :resource:`repository ` .. versionadded:: 2.6 diff --git a/examples/views/layout.py b/examples/views/layout.py index 08d43c1af3e2..70effc30cd31 100644 --- a/examples/views/layout.py +++ b/examples/views/layout.py @@ -28,11 +28,9 @@ async def action_row_button(self, interaction: discord.Interaction, button: disc await interaction.response.send_message('Hi!', ephemeral=True) container = discord.ui.Container( - [ - discord.ui.TextDisplay( - 'Click the above button to receive a **very special** message!', - ), - ], + discord.ui.TextDisplay( + 'Click the above button to receive a **very special** message!', + ), accent_colour=discord.Colour.blurple(), ) @@ -43,7 +41,7 @@ async def action_row_button(self, interaction: discord.Interaction, button: disc @bot.command() async def layout(ctx: commands.Context): """Sends a very special message!""" - await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any other content + await ctx.send(view=Layout()) # sending LayoutView's does not allow for sending any content, embed(s), stickers, or poll bot.run('token') From 5a1afb637ffa2a1b2e22adcec9562d2c982ae8e6 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:41:47 +0200 Subject: [PATCH 322/354] chore: Update message.component doc types --- discord/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index f6fba87cba59..0057b06f8737 100644 --- a/discord/message.py +++ b/discord/message.py @@ -488,7 +488,7 @@ class MessageSnapshot: Extra features of the the message snapshot. stickers: List[:class:`StickerItem`] A list of sticker items given to the message. - components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]] + components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`, :class:`Container`, :class:`SectionComponent`, :class:`TextDisplay`, :class:`MediaGalleryComponent`, :class:`FileComponent`, :class:`SeparatorComponent`, :class:`ThumbnailComponent`]] A list of components in the message. """ From de4d8c489d95333f311a755b37a418a9a6074763 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:40:13 +0200 Subject: [PATCH 323/354] =?UTF-8?q?fix:=20LayoutView=E2=80=99s=20duplicati?= =?UTF-8?q?ng=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/ui/view.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index a131414327e3..a3ec58928aa5 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -47,6 +47,7 @@ import sys import time import os +import copy from .item import Item, ItemCallbackType from .dynamic import DynamicItem from ..components import ( @@ -197,7 +198,7 @@ class BaseView: __discord_ui_view__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False - __view_children_items__: ClassVar[List[ItemLike]] = [] + __view_children_items__: ClassVar[Dict[str, ItemLike]] = [] def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout = timeout @@ -218,15 +219,17 @@ def __repr__(self) -> str: def _init_children(self) -> List[Item[Self]]: children = [] - for raw in self.__view_children_items__: + for name, raw in self.__view_children_items__.items(): if isinstance(raw, Item): - raw._view = self - parent = getattr(raw, '__discord_ui_parent__', None) + item = copy.deepcopy(raw) + setattr(self, name, item) + item._view = self + parent = getattr(item, '__discord_ui_parent__', None) if parent and parent._view is None: parent._view = self - if getattr(raw, '__discord_ui_update_view__', False): - raw._update_children_view(self) # type: ignore - children.append(raw) + if getattr(item, '__discord_ui_update_view__', False): + item._update_children_view(self) # type: ignore + children.append(item) else: item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) item.callback = _ViewCallback(raw, self, item) # type: ignore @@ -594,7 +597,7 @@ def __init_subclass__(cls) -> None: if len(children) > 25: raise TypeError('View cannot have more than 25 children') - cls.__view_children_items__ = list(children.values()) + cls.__view_children_items__ = children def __init__(self, *, timeout: Optional[float] = 180.0): super().__init__(timeout=timeout) @@ -715,7 +718,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: super().__init__(timeout=timeout) def __init_subclass__(cls) -> None: - children: Dict[str, Item[Any]] = {} + children: Dict[str, ItemLike] = {} callback_children: Dict[str, ItemCallbackType[Any]] = {} row = 0 @@ -732,7 +735,8 @@ def __init_subclass__(cls) -> None: if len(children) > 10: raise TypeError('LayoutView cannot have more than 10 top-level children') - cls.__view_children_items__ = list(children.values()) + list(callback_children.values()) + children.update(callback_children) + cls.__view_children_items__ = children def _is_v2(self) -> bool: return True From 5162d17d4aa85433386006930b895768e7618d3e Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:22:18 +0200 Subject: [PATCH 324/354] fix typings and errors --- discord/ui/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index a3ec58928aa5..2ece514d2d2a 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -218,6 +218,7 @@ def __repr__(self) -> str: def _init_children(self) -> List[Item[Self]]: children = [] + parents = {} for name, raw in self.__view_children_items__.items(): if isinstance(raw, Item): @@ -230,6 +231,7 @@ def _init_children(self) -> List[Item[Self]]: if getattr(item, '__discord_ui_update_view__', False): item._update_children_view(self) # type: ignore children.append(item) + parents[raw] = item else: item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__) item.callback = _ViewCallback(raw, self, item) # type: ignore @@ -237,7 +239,7 @@ def _init_children(self) -> List[Item[Self]]: setattr(self, raw.__name__, item) parent = getattr(raw, '__discord_ui_parent__', None) if parent: - parent._children.append(item) + parents.get(parent, parent)._children.append(item) continue children.append(item) @@ -586,7 +588,7 @@ class View(BaseView): def __init_subclass__(cls) -> None: super().__init_subclass__() - children: Dict[str, ItemCallbackType[Any]] = {} + children: Dict[str, ItemLike] = {} for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if hasattr(member, '__discord_ui_model_type__'): From a8285e1931f1ffdfd25cc93a775c826368a21012 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Sat, 26 Apr 2025 14:22:57 +0200 Subject: [PATCH 325/354] more typings --- discord/ui/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 2ece514d2d2a..cb82c8cfdea6 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -198,7 +198,7 @@ class BaseView: __discord_ui_view__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False __discord_ui_container__: ClassVar[bool] = False - __view_children_items__: ClassVar[Dict[str, ItemLike]] = [] + __view_children_items__: ClassVar[Dict[str, ItemLike]] = {} def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout = timeout From aa41094cc1c5c219f847079b8517d997638fbd36 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:44:20 +0200 Subject: [PATCH 326/354] fix: Non-dispatchable items breaking persistent views --- discord/ui/item.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/ui/item.py b/discord/ui/item.py index 47a31633bd0b..87cfc368133f 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -112,7 +112,9 @@ def is_dispatchable(self) -> bool: return False def is_persistent(self) -> bool: - return self._provided_custom_id + if self.is_dispatchable(): + return self._provided_custom_id + return True def __repr__(self) -> str: attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__) From 7741166e86b0ce96aa84a2ba250185fec4692689 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:59:12 +0200 Subject: [PATCH 327/354] chore: Add (View|Container|ActionRow|Section).walk_children methods --- discord/ui/action_row.py | 14 ++++++++++++++ discord/ui/container.py | 19 ++++++++++++++++++- discord/ui/section.py | 21 ++++++++++++++++++++- discord/ui/view.py | 24 +++++++++++++++++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 22df88f85618..946b6d3b9e14 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -30,6 +30,7 @@ ClassVar, Coroutine, Dict, + Generator, List, Literal, Optional, @@ -204,6 +205,19 @@ def children(self) -> List[Item[V]]: """List[:class:`Item`]: The list of children attached to this action row.""" return self._children.copy() + def walk_children(self) -> Generator[Item[V], Any, None]: + """An iterator that recursively walks through all the children of this view + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the action row. + """ + + for child in self.children: + yield child + def add_item(self, item: Item[Any]) -> Self: """Adds an item to this row. diff --git a/discord/ui/container.py b/discord/ui/container.py index 617450773a4b..0f362f898650 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -25,7 +25,7 @@ import copy import os -from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union +from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, Generator, List, Literal, Optional, Tuple, Type, TypeVar, Union from .item import Item, ItemCallbackType from .view import _component_to_item, LayoutView @@ -297,6 +297,23 @@ def from_component(cls, component: ContainerComponent) -> Self: id=component.id, ) + def walk_children(self) -> Generator[Item[V], Any, None]: + """An iterator that recursively walks through all the children of this container + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the container. + """ + + for child in self.children: + yield child + + if getattr(child, '__discord_ui_update_view__', False): + # if it has this attribute then it can contain children + yield from child.walk_children() # type: ignore + def add_item(self, item: Item[Any]) -> Self: """Adds an item to this container. diff --git a/discord/ui/section.py b/discord/ui/section.py index 355beebc9161..3f91514c7d8e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypeVar, Union, ClassVar +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar from .item import Item from .text_display import TextDisplay @@ -96,6 +96,11 @@ def __init__( def type(self) -> Literal[ComponentType.section]: return ComponentType.section + @property + def children(self) -> List[Item[V]]: + """List[:class:`Item`]: The list of children attached to this section.""" + return self._children.copy() + @property def width(self): return 5 @@ -110,6 +115,20 @@ def _is_v2(self) -> bool: def is_dispatchable(self) -> bool: return self.accessory.is_dispatchable() + def walk_children(self) -> Generator[Item[V], Any, None]: + """An iterator that recursively walks through all the children of this section. + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in this section. + """ + + for child in self.children: + yield child + yield self.accessory + def _update_children_view(self, view) -> None: self.accessory._view = view diff --git a/discord/ui/view.py b/discord/ui/view.py index cb82c8cfdea6..cbb28e0c8b07 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -638,6 +638,11 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) In order to modify and edit message components they must be converted into a :class:`View` first. + .. warning:: + + This **will not** take into account every v2 component, if you + want to edit them, use :meth:`LayoutView.from_message` instead. + Parameters ----------- message: :class:`discord.Message` @@ -770,7 +775,7 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) In order to modify and edit message components they must be converted into a :class:`LayoutView` first. - Unlike :meth:`View.from_message` this works for + Unlike :meth:`View.from_message` this converts v2 components. Parameters ----------- @@ -793,6 +798,23 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) return view + def walk_children(self): + """An iterator that recursively walks through all the children of this view + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the view. + """ + + for child in self.children: + yield child + + if getattr(child, '__discord_ui_update_view__', False): + # if it has this attribute then it can contain children + yield from child.walk_children() # type: ignore + class ViewStore: def __init__(self, state: ConnectionState): From 0621b38b1127c4e6215656dd4d088b048557ac99 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:13:57 +0200 Subject: [PATCH 328/354] chore: Update overloads typings --- discord/channel.py | 46 +++++++++++++++++++-------------------- discord/webhook/async_.py | 17 +++++++++------ discord/webhook/sync.py | 37 ++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 314c46faec4c..168eee1f4ad6 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2846,15 +2846,15 @@ async def create_thread( self, *, name: str, - auto_archive_duration: ThreadArchiveDuration = MISSING, - slowmode_delay: Optional[int] = None, - file: File = MISSING, - files: Sequence[File] = MISSING, - allowed_mentions: AllowedMentions = MISSING, - mention_author: bool = MISSING, + auto_archive_duration: ThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + file: File = ..., + files: Sequence[File] = ..., + allowed_mentions: AllowedMentions = ..., + mention_author: bool = ..., view: LayoutView, - suppress_embeds: bool = False, - reason: Optional[str] = None, + suppress_embeds: bool = ..., + reason: Optional[str] = ..., ) -> ThreadWithMessage: ... @@ -2863,21 +2863,21 @@ async def create_thread( self, *, name: str, - auto_archive_duration: ThreadArchiveDuration = MISSING, - slowmode_delay: Optional[int] = None, - content: Optional[str] = None, - tts: bool = False, - embed: Embed = MISSING, - embeds: Sequence[Embed] = MISSING, - file: File = MISSING, - files: Sequence[File] = MISSING, - stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING, - allowed_mentions: AllowedMentions = MISSING, - mention_author: bool = MISSING, - applied_tags: Sequence[ForumTag] = MISSING, - view: View = MISSING, - suppress_embeds: bool = False, - reason: Optional[str] = None, + auto_archive_duration: ThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + content: Optional[str] = ..., + tts: bool = ..., + embed: Embed = ..., + embeds: Sequence[Embed] = ..., + file: File = ..., + files: Sequence[File] = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + mention_author: bool = ..., + applied_tags: Sequence[ForumTag] = ..., + view: View = ..., + suppress_embeds: bool = ..., + reason: Optional[str] = ..., ) -> ThreadWithMessage: ... diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 897ddadd377c..df6055d9c520 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1983,7 +1983,10 @@ async def edit_message( self, message_id: int, *, + attachments: Sequence[Union[Attachment, File]] = ..., view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., ) -> WebhookMessage: ... @@ -1992,13 +1995,13 @@ async def edit_message( self, message_id: int, *, - content: Optional[str] = MISSING, - embeds: Sequence[Embed] = MISSING, - embed: Optional[Embed] = MISSING, - attachments: Sequence[Union[Attachment, File]] = MISSING, - view: Optional[View] = MISSING, - allowed_mentions: Optional[AllowedMentions] = None, - thread: Snowflake = MISSING, + content: Optional[str] = ..., + embeds: Sequence[Embed] = ..., + embed: Optional[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + view: Optional[View] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., ) -> WebhookMessage: ... diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 9c211898d812..d5295c1fc0a6 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -1035,7 +1035,7 @@ def send( .. versionadded:: 2.4 view: Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`] - The view to send with the message. This can only have URL buttons, which donnot + The view to send with the message. This can only have non-interactible items, which donnot require a state to be attached to it. If you want to send a view with any component attached to it, check :meth:`Webhook.send`. @@ -1185,6 +1185,33 @@ def fetch_message(self, id: int, /, *, thread: Snowflake = MISSING) -> SyncWebho ) return self._create_message(data, thread=thread) + @overload + def edit_message( + self, + message_id: int, + *, + attachments: Sequence[Union[Attachment, File]] = ..., + view: LayoutView, + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> SyncWebhookMessage: + ... + + @overload + def edit_message( + self, + message_id: int, + *, + content: Optional[str] = ..., + embeds: Sequence[Embed] = ..., + embed: Optional[Embed] = ..., + attachments: Sequence[Union[Attachment, File]] = ..., + view: Optional[View] = ..., + allowed_mentions: Optional[AllowedMentions] = ..., + thread: Snowflake = ..., + ) -> SyncWebhookMessage: + ... + def edit_message( self, message_id: int, @@ -1193,6 +1220,7 @@ def edit_message( embeds: Sequence[Embed] = MISSING, embed: Optional[Embed] = MISSING, attachments: Sequence[Union[Attachment, File]] = MISSING, + view: Optional[BaseView] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, thread: Snowflake = MISSING, ) -> SyncWebhookMessage: @@ -1219,6 +1247,13 @@ def edit_message( then all attachments are removed. .. versionadded:: 2.0 + view: Optional[Union[:class:`~discord.ui.View`, :class:`~discord.ui.LayoutView`]] + The updated view to update this message with. This can only have non-interactible items, which donnot + require a state to be attached to it. If ``None`` is passed then the view is removed. + + If you want to edit a webhook message with any component attached to it, check :meth:`WebhookMessage.edit`. + + .. versionadded:: 2.6 allowed_mentions: :class:`AllowedMentions` Controls the mentions being processed in this message. See :meth:`.abc.Messageable.send` for more information. From 2da3a1467b0882258ad4ecb13482ab3df30ecd1e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:26:35 +0200 Subject: [PATCH 329/354] chore: Raise LayoutView component limit to 40 and remove component limit on containers --- discord/ui/container.py | 2 -- discord/ui/view.py | 60 ++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 0f362f898650..4aad65ca4c3f 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -136,8 +136,6 @@ def __init__( self._children: List[Item[V]] = self._init_children() if children is not MISSING: - if len(children) + len(self._children) > 10: - raise ValueError('maximum number of children exceeded') for child in children: self.add_item(child) diff --git a/discord/ui/view.py b/discord/ui/view.py index cbb28e0c8b07..91f96e508e52 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -209,6 +209,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self.__total_children: int = len(list(self.walk_children())) def _is_v2(self) -> bool: return False @@ -346,9 +347,14 @@ def add_item(self, item: Item[Any]) -> Self: raise ValueError('v2 items cannot be added to this view') item._view = self + added = 1 if getattr(item, '__discord_ui_update_view__', False): item._update_children_view(self) # type: ignore + added += len(list(item.walk_children())) # type: ignore + + if self._is_v2() and self.__total_children + added > 40: + raise ValueError('maximum number of children exceeded') self._children.append(item) return self @@ -369,6 +375,16 @@ def remove_item(self, item: Item[Any]) -> Self: self._children.remove(item) except ValueError: pass + else: + removed = 1 + if getattr(item, '__discord_ui_update_view__', False): + removed += len(list(item.walk_children())) # type: ignore + + if self.__total_children - removed < 0: + self.__total_children = 0 + else: + self.__total_children -= removed + return self def clear_items(self) -> Self: @@ -378,6 +394,7 @@ def clear_items(self) -> Self: chaining. """ self._children.clear() + self.__total_children = 0 return self def get_item_by_id(self, id: int, /) -> Optional[Item[Self]]: @@ -568,6 +585,23 @@ async def wait(self) -> bool: """ return await self.__stopped + def walk_children(self): + """An iterator that recursively walks through all the children of this view + and it's children, if applicable. + + Yields + ------ + :class:`Item` + An item in the view. + """ + + for child in self.children: + yield child + + if getattr(child, '__discord_ui_update_view__', False): + # if it has this attribute then it can contain children + yield from child.walk_children() # type: ignore + class View(BaseView): """Represents a UI view. @@ -723,6 +757,10 @@ class LayoutView(BaseView): def __init__(self, *, timeout: Optional[float] = 180.0) -> None: super().__init__(timeout=timeout) + self.__total_children: int = len(list(self.walk_children())) + + if self.__total_children > 40: + raise ValueError('maximum number of children exceeded') def __init_subclass__(cls) -> None: children: Dict[str, ItemLike] = {} @@ -739,9 +777,6 @@ def __init_subclass__(cls) -> None: elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): callback_children[name] = member - if len(children) > 10: - raise TypeError('LayoutView cannot have more than 10 top-level children') - children.update(callback_children) cls.__view_children_items__ = children @@ -761,7 +796,7 @@ def to_components(self): return components def add_item(self, item: Item[Any]) -> Self: - if len(self._children) >= 10: + if self.__total_children >= 40: raise ValueError('maximum number of children exceeded') super().add_item(item) return self @@ -798,23 +833,6 @@ def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) return view - def walk_children(self): - """An iterator that recursively walks through all the children of this view - and it's children, if applicable. - - Yields - ------ - :class:`Item` - An item in the view. - """ - - for child in self.children: - yield child - - if getattr(child, '__discord_ui_update_view__', False): - # if it has this attribute then it can contain children - yield from child.walk_children() # type: ignore - class ViewStore: def __init__(self, state: ConnectionState): From d41d7111a76ba67454ecc73ccdac85377acbd75f Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:38:18 +0200 Subject: [PATCH 330/354] list -> tuple --- discord/ui/view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index 91f96e508e52..e7a17c2d75e0 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -209,7 +209,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() - self.__total_children: int = len(list(self.walk_children())) + self.__total_children: int = len(tuple(self.walk_children())) def _is_v2(self) -> bool: return False @@ -351,7 +351,7 @@ def add_item(self, item: Item[Any]) -> Self: if getattr(item, '__discord_ui_update_view__', False): item._update_children_view(self) # type: ignore - added += len(list(item.walk_children())) # type: ignore + added += len(tuple(item.walk_children())) # type: ignore if self._is_v2() and self.__total_children + added > 40: raise ValueError('maximum number of children exceeded') @@ -378,7 +378,7 @@ def remove_item(self, item: Item[Any]) -> Self: else: removed = 1 if getattr(item, '__discord_ui_update_view__', False): - removed += len(list(item.walk_children())) # type: ignore + removed += len(tuple(item.walk_children())) # type: ignore if self.__total_children - removed < 0: self.__total_children = 0 From 50c40a20b377f02a412b660a02b4f07b5bcba350 Mon Sep 17 00:00:00 2001 From: DA344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:14:48 +0200 Subject: [PATCH 331/354] fix: Change send type to None in Section.walk_children return type Co-authored-by: Michael H --- discord/ui/section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/section.py b/discord/ui/section.py index 3f91514c7d8e..cefc4cc4085e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -115,7 +115,7 @@ def _is_v2(self) -> bool: def is_dispatchable(self) -> bool: return self.accessory.is_dispatchable() - def walk_children(self) -> Generator[Item[V], Any, None]: + def walk_children(self) -> Generator[Item[V], None, None]: """An iterator that recursively walks through all the children of this section. and it's children, if applicable. From b0b332a2e0709649b91b7aa6dfc08622e7d67939 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:16:13 +0200 Subject: [PATCH 332/354] fix: Add/Modify View/Container.walk_children return types --- discord/ui/container.py | 2 +- discord/ui/view.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 4aad65ca4c3f..a367c96e51bb 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -295,7 +295,7 @@ def from_component(cls, component: ContainerComponent) -> Self: id=component.id, ) - def walk_children(self) -> Generator[Item[V], Any, None]: + def walk_children(self) -> Generator[Item[V], None, None]: """An iterator that recursively walks through all the children of this container and it's children, if applicable. diff --git a/discord/ui/view.py b/discord/ui/view.py index e7a17c2d75e0..2217474c589e 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -30,6 +30,7 @@ ClassVar, Coroutine, Dict, + Generator, Iterator, List, Optional, @@ -585,7 +586,7 @@ async def wait(self) -> bool: """ return await self.__stopped - def walk_children(self): + def walk_children(self) -> Generator[Item[Any], None, None]: """An iterator that recursively walks through all the children of this view and it's children, if applicable. From 9c745bb751269b8453f335b0894c35c5708f255e Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:20:03 +0200 Subject: [PATCH 333/354] chore: run black --- discord/ui/container.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index a367c96e51bb..f87915e4e9d8 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -25,7 +25,21 @@ import copy import os -from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Dict, Generator, List, Literal, Optional, Tuple, Type, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Coroutine, + Dict, + Generator, + List, + Literal, + Optional, + Tuple, + Type, + TypeVar, + Union, +) from .item import Item, ItemCallbackType from .view import _component_to_item, LayoutView From 4044b2c97f6b1eaea32cf4c836eeb4cf1969910d Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:20:23 +0200 Subject: [PATCH 334/354] chore: add *children param and validation for children --- discord/ui/action_row.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 946b6d3b9e14..e73d731de94a 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -134,9 +134,19 @@ async def row_button(self, interaction: discord.Interaction, button: discord.ui. __discord_ui_action_row__: ClassVar[bool] = True __discord_ui_update_view__: ClassVar[bool] = True - def __init__(self, *, id: Optional[int] = None) -> None: + def __init__( + self, + *children: Item[V], + id: Optional[int] = None, + ) -> None: super().__init__() - self._children: List[Item[Any]] = self._init_children() + self._weight: int = 0 + self._children: List[Item[V]] = self._init_children() + self._children.extend(children) + self._weight += sum(i.width for i in children) + + if self._weight > 5: + raise ValueError('maximum number of children exceeded') self.id = id @@ -162,6 +172,7 @@ def _init_children(self) -> List[Item[Any]]: item.callback = _ActionRowCallback(func, self, item) # type: ignore item._parent = getattr(func, '__discord_ui_parent__', self) setattr(self, func.__name__, item) + self._weight += item.width children.append(item) return children @@ -182,7 +193,7 @@ def is_dispatchable(self) -> bool: def _update_children_view(self, view: LayoutView) -> None: for child in self._children: - child._view = view + child._view = view # pyright: ignore[reportAttributeAccessIssue] def _is_v2(self) -> bool: # although it is not really a v2 component the only usecase here is for From 145af2f67f662717b60c8c35b8f1dd2fd22aa05f Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 1 May 2025 11:24:35 +0200 Subject: [PATCH 335/354] chore: update docstrings --- discord/ui/action_row.py | 2 ++ discord/ui/container.py | 5 ++--- discord/ui/file.py | 2 +- discord/ui/media_gallery.py | 2 +- discord/ui/section.py | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index e73d731de94a..1eeb75da4aad 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -126,6 +126,8 @@ async def row_button(self, interaction: discord.Interaction, button: discord.ui. Parameters ---------- + *children: :class:`Item` + The initial children of this action row. id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/container.py b/discord/ui/container.py index f87915e4e9d8..adfbb549adb4 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -111,9 +111,8 @@ class MyView(ui.LayoutView): Parameters ---------- - children: List[:class:`Item`] - The initial children of this container. Can have up to 10 - items. + *children: List[:class:`Item`] + The initial children of this container. accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]] The colour of the container. Defaults to ``None``. accent_color: Optional[Union[:class:`.Colour`, :class:`int`]] diff --git a/discord/ui/file.py b/discord/ui/file.py index 341860cc739d..ccc7a0fdcb6a 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -63,7 +63,7 @@ class MyView(ui.LayoutView): media: Union[:class:`str`, :class:`.UnfurledMediaItem`] This file's media. If this is a string it must point to a local file uploaded within the parent view of this item, and must - meet the ``attachment://file-name.extension`` structure. + meet the ``attachment://filename.extension`` structure. spoiler: :class:`bool` Whether to flag this file as a spoiler. Defaults to ``False``. row: Optional[:class:`int`] diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index badd495e0802..b7c92f0475fa 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -53,7 +53,7 @@ class MediaGallery(Item[V]): Parameters ---------- - items: List[:class:`.MediaGalleryItem`] + *items: :class:`.MediaGalleryItem` The initial items of this gallery. row: Optional[:class:`int`] The relative row this media gallery belongs to. By default diff --git a/discord/ui/section.py b/discord/ui/section.py index cefc4cc4085e..7a5fd2afaacb 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -50,7 +50,7 @@ class Section(Item[V]): Parameters ---------- - children: List[Union[:class:`str`, :class:`TextDisplay`]] + *children: Union[:class:`str`, :class:`TextDisplay`] The text displays of this section. Up to 3. accessory: :class:`Item` The section accessory. From 27db09adcfdb5a29371960f414567c6cb47cf267 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 1 May 2025 11:42:21 +0200 Subject: [PATCH 336/354] chore: overloads --- discord/ext/commands/context.py | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 1e957feb4f16..3a7204e9b7bc 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -855,7 +855,33 @@ async def defer(self, *, ephemeral: bool = False) -> None: async def send( self, *, + file: File = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., + ) -> Message: + ... + + @overload + async def send( + self, + *, + files: Sequence[File] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., view: LayoutView, + suppress_embeds: bool = ..., + ephemeral: bool = ..., + silent: bool = ..., ) -> Message: ... @@ -873,7 +899,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -895,7 +921,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -917,7 +943,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., @@ -939,7 +965,7 @@ async def send( allowed_mentions: AllowedMentions = ..., reference: Union[Message, MessageReference, PartialMessage] = ..., mention_author: bool = ..., - view: BaseView = ..., + view: View = ..., suppress_embeds: bool = ..., ephemeral: bool = ..., silent: bool = ..., From 03964172d33552ca7be6952e1197e7ac36ad0120 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 1 May 2025 16:14:57 +0200 Subject: [PATCH 337/354] rows --- discord/ui/action_row.py | 9 +++++++++ discord/ui/container.py | 2 +- discord/ui/file.py | 2 +- discord/ui/item.py | 2 +- discord/ui/media_gallery.py | 2 +- discord/ui/section.py | 2 +- discord/ui/separator.py | 2 +- discord/ui/text_display.py | 2 +- discord/ui/thumbnail.py | 2 +- 9 files changed, 17 insertions(+), 8 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 1eeb75da4aad..e54522ec613a 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -128,6 +128,13 @@ async def row_button(self, interaction: discord.Interaction, button: discord.ui. ---------- *children: :class:`Item` The initial children of this action row. + row: Optional[:class:`int`] + The relative row this action row belongs to. By default + items are arranged automatically into those rows. If you'd + like to control the relative positioning of the row then + passing an index is advised. For example, row=1 will show + up before row=2. Defaults to ``None``, which is automatic + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ @@ -139,6 +146,7 @@ async def row_button(self, interaction: discord.Interaction, button: discord.ui. def __init__( self, *children: Item[V], + row: Optional[int] = None, id: Optional[int] = None, ) -> None: super().__init__() @@ -151,6 +159,7 @@ def __init__( raise ValueError('maximum number of children exceeded') self.id = id + self.row = row def __init_subclass__(cls) -> None: super().__init_subclass__() diff --git a/discord/ui/container.py b/discord/ui/container.py index adfbb549adb4..c5890722c46c 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -126,7 +126,7 @@ class MyView(ui.LayoutView): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/file.py b/discord/ui/file.py index ccc7a0fdcb6a..5d9014e72acb 100644 --- a/discord/ui/file.py +++ b/discord/ui/file.py @@ -72,7 +72,7 @@ class MyView(ui.LayoutView): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/item.py b/discord/ui/item.py index 87cfc368133f..c73d8e7621ba 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -80,7 +80,7 @@ def __init__(self): # only called upon edit and we're mainly interested during initial creation time. self._provided_custom_id: bool = False self._id: Optional[int] = None - self._max_row: int = 5 if not self._is_v2() else 10 + self._max_row: int = 5 if not self._is_v2() else 40 self._parent: Optional[Item] = None if self._is_v2(): diff --git a/discord/ui/media_gallery.py b/discord/ui/media_gallery.py index b7c92f0475fa..bbecc96494db 100644 --- a/discord/ui/media_gallery.py +++ b/discord/ui/media_gallery.py @@ -61,7 +61,7 @@ class MediaGallery(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/section.py b/discord/ui/section.py index 7a5fd2afaacb..2d4acdc3a804 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -60,7 +60,7 @@ class Section(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/separator.py b/discord/ui/separator.py index 48908df9d6eb..e7d75a998b95 100644 --- a/discord/ui/separator.py +++ b/discord/ui/separator.py @@ -59,7 +59,7 @@ class Separator(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/text_display.py b/discord/ui/text_display.py index beff74c6eac3..9ba7f294e4d0 100644 --- a/discord/ui/text_display.py +++ b/discord/ui/text_display.py @@ -56,7 +56,7 @@ class TextDisplay(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ diff --git a/discord/ui/thumbnail.py b/discord/ui/thumbnail.py index f639269918a4..67f8e4c7629f 100644 --- a/discord/ui/thumbnail.py +++ b/discord/ui/thumbnail.py @@ -61,7 +61,7 @@ class Thumbnail(Item[V]): like to control the relative positioning of the row then passing an index is advised. For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic - ordering. The row number must be between 0 and 9 (i.e. zero indexed) + ordering. The row number must be between 0 and 39 (i.e. zero indexed) id: Optional[:class:`int`] The ID of this component. This must be unique across the view. """ From 7012cec96a35c925051b3525d1d56372376c3db2 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 6 May 2025 16:43:05 +0200 Subject: [PATCH 338/354] fix: LayoutView.__total_children being incorrectly set when adding/removing items from an item --- discord/ui/action_row.py | 10 ++++++++++ discord/ui/container.py | 17 +++++++++++++++++ discord/ui/section.py | 11 +++++++++++ discord/ui/view.py | 2 ++ 4 files changed, 40 insertions(+) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index e54522ec613a..a72d4db1e9ef 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -268,6 +268,10 @@ def add_item(self, item: Item[Any]) -> Self: item._view = self._view item._parent = self self._children.append(item) + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children += 1 + return self def remove_item(self, item: Item[Any]) -> Self: @@ -286,6 +290,10 @@ def remove_item(self, item: Item[Any]) -> Self: self._children.remove(item) except ValueError: pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= 1 + return self def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: @@ -314,6 +322,8 @@ def clear_items(self) -> Self: This function returns the class instance to allow for fluent-style chaining. """ + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(self._children) self._children.clear() return self diff --git a/discord/ui/container.py b/discord/ui/container.py index c5890722c46c..d9eb4f35d093 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -360,9 +360,17 @@ def add_item(self, item: Item[Any]) -> Self: else: self.__dispatchable.append(item) + is_layout_view = self._view and getattr(self._view, '__discord_ui_layout_view__', False) + if getattr(item, '__discord_ui_update_view__', False): item._update_children_view(self.view) # type: ignore + if is_layout_view: + self._view.__total_children += len(tuple(item.walk_children())) # type: ignore + else: + if is_layout_view: + self._view.__total_children += 1 # type: ignore + item._view = self.view item._parent = self return self @@ -383,6 +391,12 @@ def remove_item(self, item: Item[Any]) -> Self: self._children.remove(item) except ValueError: pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + if getattr(item, '__discord_ui_update_view__', False): + self._view.__total_children -= len(tuple(item.walk_children())) # type: ignore + else: + self._view.__total_children -= 1 return self def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: @@ -411,5 +425,8 @@ def clear_items(self) -> Self: This function returns the class instance to allow for fluent-style chaining. """ + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(tuple(self.walk_children())) self._children.clear() return self diff --git a/discord/ui/section.py b/discord/ui/section.py index 2d4acdc3a804..70c5a778c7a9 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -162,6 +162,10 @@ def add_item(self, item: Union[str, Item[Any]]) -> Self: item._view = self.view item._parent = self self._children.append(item) + + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children += 1 + return self def remove_item(self, item: Item[Any]) -> Self: @@ -180,6 +184,10 @@ def remove_item(self, item: Item[Any]) -> Self: self._children.remove(item) except ValueError: pass + else: + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= 1 + return self def get_item_by_id(self, id: int, /) -> Optional[Item[V]]: @@ -208,6 +216,9 @@ def clear_items(self) -> Self: This function returns the class instance to allow for fluent-style chaining. """ + if self._view and getattr(self._view, '__discord_ui_layout_view__', False): + self._view.__total_children -= len(self._children) + 1 # the + 1 is the accessory + self._children.clear() return self diff --git a/discord/ui/view.py b/discord/ui/view.py index 2217474c589e..d8c21354c560 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -756,6 +756,8 @@ class LayoutView(BaseView): If ``None`` then there is no timeout. """ + __discord_ui_layout_view__: ClassVar[bool] = True + def __init__(self, *, timeout: Optional[float] = 180.0) -> None: super().__init__(timeout=timeout) self.__total_children: int = len(list(self.walk_children())) From e29c10d18680882d2c8c155a6d3a6a58f98d169c Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 6 May 2025 16:46:50 +0200 Subject: [PATCH 339/354] fix: Webhook.send overloads missing ephemeral kwarg --- discord/webhook/async_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index df6055d9c520..104da78cab21 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1611,6 +1611,7 @@ async def send( *, username: str = MISSING, avatar_url: Any = MISSING, + ephemeral: bool = MISSING, file: File = MISSING, files: Sequence[File] = MISSING, allowed_mentions: AllowedMentions = MISSING, @@ -1630,6 +1631,7 @@ async def send( *, username: str = MISSING, avatar_url: Any = MISSING, + ephemeral: bool = MISSING, file: File = MISSING, files: Sequence[File] = MISSING, allowed_mentions: AllowedMentions = MISSING, From 6122b32dae10ebeb6ad5989675836ea0b41d8631 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 6 May 2025 16:54:13 +0200 Subject: [PATCH 340/354] fix: Sorting LayoutView children defaulting to 0 instead of sys.maxsize --- discord/ui/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/ui/view.py b/discord/ui/view.py index d8c21354c560..0f9a552a3d9b 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -791,7 +791,8 @@ def to_components(self): # sorted by row, which in LayoutView indicates the position of the component in the # payload instead of in which ActionRow it should be placed on. - for child in sorted(self._children, key=lambda i: i._rendered_row or 0): + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): components.append( child.to_component_dict(), ) From 7b5f247502ee0782c5cc978a3fe5141641dc1221 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Tue, 6 May 2025 16:54:55 +0200 Subject: [PATCH 341/354] chore: Add call to super().__init_subclass__() --- discord/ui/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/ui/view.py b/discord/ui/view.py index 0f9a552a3d9b..4de1d076647b 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -766,6 +766,8 @@ def __init__(self, *, timeout: Optional[float] = 180.0) -> None: raise ValueError('maximum number of children exceeded') def __init_subclass__(cls) -> None: + super().__init_subclass__() + children: Dict[str, ItemLike] = {} callback_children: Dict[str, ItemCallbackType[Any]] = {} From cf08c0e78f03da0f7006c887f519ef3d0f955d18 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Wed, 7 May 2025 15:48:03 +0200 Subject: [PATCH 342/354] chore: Remove ValueError on Container.add_item --- discord/ui/container.py | 6 ------ discord/ui/view.py | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index d9eb4f35d093..dab8f58ee215 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -340,13 +340,7 @@ def add_item(self, item: Item[Any]) -> Self: ------ TypeError An :class:`Item` was not passed. - ValueError - Maximum number of children has been exceeded (10). """ - - if len(self._children) >= 10: - raise ValueError('maximum number of children exceeded') - if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') diff --git a/discord/ui/view.py b/discord/ui/view.py index 4de1d076647b..2b72b89482df 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -337,7 +337,7 @@ def add_item(self, item: Item[Any]) -> Self: TypeError An :class:`Item` was not passed. ValueError - Maximum number of children has been exceeded (25), the + Maximum number of children has been exceeded, the row the item is trying to be added to is full or the item you tried to add is not allowed in this View. """ @@ -803,7 +803,7 @@ def to_components(self): def add_item(self, item: Item[Any]) -> Self: if self.__total_children >= 40: - raise ValueError('maximum number of children exceeded') + raise ValueError('maximum number of children exceeded (40)') super().add_item(item) return self From 4ca483efdb6b6d22faed5b3c24850241e651cc49 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Thu, 8 May 2025 19:11:10 +0200 Subject: [PATCH 343/354] chore: fix is_persistent and default to sys.maxsize instead of 0 on sorting key --- discord/ui/action_row.py | 9 +++++++-- discord/ui/container.py | 8 +++++++- discord/ui/section.py | 6 +++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index a72d4db1e9ef..47903a4be1ec 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -23,6 +23,7 @@ """ from __future__ import annotations +import sys from typing import ( TYPE_CHECKING, Any, @@ -202,6 +203,9 @@ def _update_store_data(self, dispatch_info: Dict, dynamic_items: Dict) -> bool: def is_dispatchable(self) -> bool: return any(c.is_dispatchable() for c in self.children) + def is_persistent(self) -> bool: + return self.is_dispatchable() and all(c.is_persistent() for c in self.children) + def _update_children_view(self, view: LayoutView) -> None: for child in self._children: child._view = view # pyright: ignore[reportAttributeAccessIssue] @@ -330,8 +334,9 @@ def clear_items(self) -> Self: def to_component_dict(self) -> Dict[str, Any]: components = [] - for item in self._children: - components.append(item.to_component_dict()) + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): + components.append(child.to_component_dict()) base = { 'type': self.type.value, diff --git a/discord/ui/container.py b/discord/ui/container.py index dab8f58ee215..174a2ec027fc 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -25,6 +25,7 @@ import copy import os +import sys from typing import ( TYPE_CHECKING, Any, @@ -210,6 +211,9 @@ def _init_children(self) -> List[Item[Any]]: def is_dispatchable(self) -> bool: return bool(self.__dispatchable) + def is_persistent(self) -> bool: + return self.is_dispatchable() and all(c.is_persistent() for c in self.children) + def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -263,7 +267,9 @@ def _is_v2(self) -> bool: def to_components(self) -> List[Dict[str, Any]]: components = [] - for child in sorted(self._children, key=lambda i: i._rendered_row or 0): + + key = lambda i: i._rendered_row or i._row or sys.maxsize + for child in sorted(self._children, key=key): components.append(child.to_component_dict()) return components diff --git a/discord/ui/section.py b/discord/ui/section.py index 70c5a778c7a9..28688152fc1e 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -23,6 +23,7 @@ """ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional, TypeVar, Union, ClassVar from .item import Item @@ -115,6 +116,9 @@ def _is_v2(self) -> bool: def is_dispatchable(self) -> bool: return self.accessory.is_dispatchable() + def is_persistent(self) -> bool: + return self.is_dispatchable() and self.accessory.is_persistent() + def walk_children(self) -> Generator[Item[V], None, None]: """An iterator that recursively walks through all the children of this section. and it's children, if applicable. @@ -239,7 +243,7 @@ def to_component_dict(self) -> Dict[str, Any]: c.to_component_dict() for c in sorted( self._children, - key=lambda i: i._rendered_row or 0, + key=lambda i: i._rendered_row or sys.maxsize, ) ], 'accessory': self.accessory.to_component_dict(), From 4103a976356a7570b311cb3d929343e05c2ce178 Mon Sep 17 00:00:00 2001 From: DA-344 <108473820+DA-344@users.noreply.github.com> Date: Fri, 9 May 2025 22:19:43 +0200 Subject: [PATCH 344/354] things --- discord/ui/container.py | 2 +- discord/ui/view.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/ui/container.py b/discord/ui/container.py index 174a2ec027fc..72892d3a08eb 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -112,7 +112,7 @@ class MyView(ui.LayoutView): Parameters ---------- - *children: List[:class:`Item`] + *children: :class:`Item` The initial children of this container. accent_colour: Optional[Union[:class:`.Colour`, :class:`int`]] The colour of the container. Defaults to ``None``. diff --git a/discord/ui/view.py b/discord/ui/view.py index 2b72b89482df..7ecc9da1e482 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -776,7 +776,9 @@ def __init_subclass__(cls) -> None: for base in reversed(cls.__mro__): for name, member in base.__dict__.items(): if isinstance(member, Item): - member._rendered_row = member._row or row + if member._row is None: + member._row = row + member._rendered_row = member._row children[name] = member row += 1 elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None): From 636f232e1fe8a0fc8b179116a26092bead895f36 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 10 May 2025 19:23:05 +0100 Subject: [PATCH 345/354] Added Arguments and Permissions --- discord/ext/commands/core.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 372fcbedfdf6..dc2b96ebf5ba 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -24,6 +24,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass import datetime import functools import inspect @@ -42,6 +43,8 @@ Type, TypeVar, Union, + get_args, + get_origin, overload, ) import re @@ -292,6 +295,11 @@ def __next__(self) -> discord.Attachment: def is_empty(self) -> bool: return self.index >= len(self.data) +@dataclass +class CommandArgument: + name: str + optional: bool + class Command(_BaseCommand, Generic[CogT, P, T]): r"""A class that implements the protocol for a bot text command. @@ -512,6 +520,37 @@ def callback( self.params: Dict[str, Parameter] = get_signature_parameters(function, globalns) + @discord.utils.cached_property + def arguments(self: Command) -> List[CommandArgument]: + def get_parameter_name(name: str, annotation: Optional[type]) -> str: + if annotation is not None and get_origin(annotation) is Literal: + literal_values = get_args(annotation) + if len(literal_values) > 1: + return ', '.join([f"'{value}'" for value in literal_values[:-1]]) + f" or '{literal_values[-1]}'" + else: + return f'`{literal_values[0]}`' # type: ignore + return name.replace('_', ' ') + + return [ + CommandArgument( + name=get_parameter_name(name, param.annotation), + optional=(get_origin(param.annotation) is Union and type(None) in get_args(param.annotation)), + ) + for name, param in list(inspect.signature(self.callback).parameters.items())[2:] + ] + + @discord.utils.cached_property + def permissions(self) -> List[str]: + return [ + perm + for check in self.checks + if getattr(check, '__closure__', None) + for cell in check.__closure__ # type: ignore + if isinstance(cell.cell_contents, dict) + for perm, val in cell.cell_contents.items() + if val + ] or ['N/A'] + def add_check(self, func: UserCheck[Context[Any]], /) -> None: """Adds a check to the command. From 91bf99fe6841b5727c852b547a7b2755107890c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=96adammbh?= Date: Fri, 27 Jun 2025 16:17:31 +0400 Subject: [PATCH 346/354] fix: new discord codes that are not standard (creds: DA-344) --- discord/gateway.py | 18 +++++++++++++++--- discord/voice_state.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/discord/gateway.py b/discord/gateway.py index 44656df03633..8e753af58512 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -212,6 +212,9 @@ def ack(self) -> None: class VoiceKeepAliveHandler(KeepAliveHandler): + if TYPE_CHECKING: + ws: DiscordVoiceWebSocket + def __init__(self, *args: Any, **kwargs: Any) -> None: name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}') super().__init__(*args, name=name, **kwargs) @@ -223,7 +226,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def get_payload(self) -> Dict[str, Any]: return { 'op': self.ws.HEARTBEAT, - 'd': int(time.time() * 1000), + 'd': { + 't': int(time.time() * 1000), + 'seq_ack': self.ws.seq_ack, + }, } def ack(self) -> None: @@ -830,6 +836,8 @@ def __init__( self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._close_code: Optional[int] = None self.secret_key: Optional[List[int]] = None + # defaulting to -1 + self.seq_ack: int = -1 if hook: self._hook = hook # type: ignore @@ -850,6 +858,7 @@ async def resume(self) -> None: 'token': state.token, 'server_id': str(state.server_id), 'session_id': state.session_id, + 'seq_ack': self.seq_ack, }, } await self.send_as_json(payload) @@ -874,14 +883,16 @@ async def from_connection_state( *, resume: bool = False, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None, + seq_ack: int = -1, ) -> Self: """Creates a voice websocket for the :class:`VoiceClient`.""" - gateway = f'wss://{state.endpoint}/?v=4' + gateway = f'wss://{state.endpoint}/?v=8' client = state.voice_client http = client._state.http socket = await http.ws_connect(gateway, compress=15) ws = cls(socket, loop=client.loop, hook=hook) ws.gateway = gateway + ws.seq_ack = seq_ack ws._connection = state ws._max_heartbeat_timeout = 60.0 ws.thread_id = threading.get_ident() @@ -934,6 +945,7 @@ async def received_message(self, msg: Dict[str, Any]) -> None: _log.debug('Voice websocket frame received: %s', msg) op = msg['op'] data = msg['d'] # According to Discord this key is always given + self.seq_ack = msg.get('seq', self.seq_ack) # this key could not be given if op == self.READY: await self.initial_connection(data) @@ -1043,4 +1055,4 @@ async def close(self, code: int = 1000) -> None: self._keep_alive.stop() self._close_code = code - await self.ws.close(code=code) + await self.ws.close(code=code) \ No newline at end of file diff --git a/discord/voice_state.py b/discord/voice_state.py index 956f639b8e0a..5f37a8de7fa8 100644 --- a/discord/voice_state.py +++ b/discord/voice_state.py @@ -321,7 +321,7 @@ async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: ) return - self.endpoint, _, _ = endpoint.rpartition(':') + self.endpoint = endpoint if self.endpoint.startswith('wss://'): # Just in case, strip it off since we're going to add it later self.endpoint = self.endpoint[6:] @@ -574,7 +574,10 @@ async def _voice_disconnect(self) -> None: self._disconnected.clear() async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: - ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook) + seq_ack = -1 + if self.ws is not MISSING: + seq_ack = self.ws.seq_ack + ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook, seq_ack=seq_ack) self.state = ConnectionFlowState.websocket_connected return ws @@ -603,8 +606,8 @@ async def _poll_voice_ws(self, reconnect: bool) -> None: # The following close codes are undocumented so I will document them here. # 1000 - normal closure (obviously) # 4014 - we were externally disconnected (voice channel deleted, we were moved, etc) - # 4015 - voice server has crashed - if exc.code in (1000, 4015): + # 4015 - voice server has crashed, we should resume + if exc.code == 1000: # Don't call disconnect a second time if the websocket closed from a disconnect call if not self._expecting_disconnect: _log.info('Disconnecting from voice normally, close code %d.', exc.code) @@ -631,6 +634,25 @@ async def _poll_voice_ws(self, reconnect: bool) -> None: else: continue + if exc.code == 4015: + _log.info('Disconnected from voice, attempting a resume...') + try: + await self._connect( + reconnect=reconnect, + timeout=self.timeout, + self_deaf=(self.self_voice_state or self).self_deaf, + self_mute=(self.self_voice_state or self).self_mute, + resume=True, + ) + except asyncio.TimeoutError: + _log.info('Could not resume the voice connection... Disconnecting...') + if self.state is not ConnectionFlowState.disconnected: + await self.disconnect() + break + else: + _log.info('Successfully resumed voice connection') + continue + _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') if not reconnect: @@ -685,4 +707,4 @@ async def _move_to(self, channel: abc.Snowflake) -> None: self.state = ConnectionFlowState.set_guild_voice_state def _update_voice_channel(self, channel_id: Optional[int]) -> None: - self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore + self.voice_client.channel = channel_id and self.guild.get_channel(channel_id) # type: ignore \ No newline at end of file From c472d2db8e6e80494991f2270c5acdb7fa46bc89 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:22:08 +0100 Subject: [PATCH 347/354] Modify Current Member --- discord/http.py | 33 +++++++++++------------- discord/member.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/discord/http.py b/discord/http.py index 4e12de8bd47c..a678034620d2 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1166,6 +1166,10 @@ def edit_member( r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, json=fields, reason=reason) + def edit_self(self, guild_id: Snowflake, **fields: Any) -> Response[member.MemberWithUser]: + r = Route(method='PATCH', path='/guilds/{guild.id}/members/@me', guild_id_id=guild_id) + return self.request(r, json=fields) + def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: return self.request(Route('GET', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)) @@ -2019,22 +2023,19 @@ def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: Literal[True] - ) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]: - ... + ) -> Response[List[scheduled_event.GuildScheduledEventWithUserCount]]: ... @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: Literal[False] - ) -> Response[List[scheduled_event.GuildScheduledEvent]]: - ... + ) -> Response[List[scheduled_event.GuildScheduledEvent]]: ... @overload def get_scheduled_events( self, guild_id: Snowflake, with_user_count: bool ) -> Union[ Response[List[scheduled_event.GuildScheduledEventWithUserCount]], Response[List[scheduled_event.GuildScheduledEvent]] - ]: - ... + ]: ... def get_scheduled_events(self, guild_id: Snowflake, with_user_count: bool) -> Response[Any]: params = {'with_user_count': int(with_user_count)} @@ -2063,20 +2064,19 @@ def create_guild_scheduled_event( @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[True] - ) -> Response[scheduled_event.GuildScheduledEventWithUserCount]: - ... + ) -> Response[scheduled_event.GuildScheduledEventWithUserCount]: ... @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: Literal[False] - ) -> Response[scheduled_event.GuildScheduledEvent]: - ... + ) -> Response[scheduled_event.GuildScheduledEvent]: ... @overload def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool - ) -> Union[Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent]]: - ... + ) -> Union[ + Response[scheduled_event.GuildScheduledEventWithUserCount], Response[scheduled_event.GuildScheduledEvent] + ]: ... def get_scheduled_event( self, guild_id: Snowflake, guild_scheduled_event_id: Snowflake, with_user_count: bool @@ -2146,8 +2146,7 @@ def get_scheduled_event_users( with_member: Literal[True], before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Response[scheduled_event.ScheduledEventUsersWithMember]: - ... + ) -> Response[scheduled_event.ScheduledEventUsersWithMember]: ... @overload def get_scheduled_event_users( @@ -2158,8 +2157,7 @@ def get_scheduled_event_users( with_member: Literal[False], before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Response[scheduled_event.ScheduledEventUsers]: - ... + ) -> Response[scheduled_event.ScheduledEventUsers]: ... @overload def get_scheduled_event_users( @@ -2170,8 +2168,7 @@ def get_scheduled_event_users( with_member: bool, before: Optional[Snowflake] = ..., after: Optional[Snowflake] = ..., - ) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]: - ... + ) -> Union[Response[scheduled_event.ScheduledEventUsersWithMember], Response[scheduled_event.ScheduledEventUsers]]: ... def get_scheduled_event_users( self, diff --git a/discord/member.py b/discord/member.py index 6af1571f4d34..0740fa79f8cd 100644 --- a/discord/member.py +++ b/discord/member.py @@ -34,7 +34,7 @@ from . import utils from .asset import Asset -from .utils import MISSING +from .utils import MISSING, _bytes_to_base64_data from .user import BaseUser, ClientUser, User, _UserTag from .permissions import Permissions from .enums import Status @@ -787,6 +787,69 @@ async def kick(self, *, reason: Optional[str] = None) -> None: """ await self.guild.kick(self, reason=reason) + async def edit_me( + self, *, nick: Optional[str] = MISSING, banner: Optional[bytes] = MISSING, avatar: Optional[bytes] = MISSING + ) -> Optional[Member]: + """|coro| + + Edits the current client member profile. + + .. note:: + To upload an avatar, or banner, a :term:`py:bytes-like object` must be passed in that + representes the image being uploaded. If this is done through a file + then the file must be opened via ``open('filename`, 'rb')`` and + the :term:`py:bytes-like object` is given through the use of ``fp.read()``. + + Parameters + ----------- + nick: Optional[:class:`str`] + The nickname you wish to change to. + Could be ``None`` to denote no nick change. + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + + Returns + ----------- + Optional[:class:`Member`] + The newly modified client member. + """ + + member = self.guild.me + + # Honestly haven't got a clue if just doing the first one is fine or not. + if not (nick or banner or avatar) or (nick is MISSING and banner is MISSING and avatar is MISSING): + return member + + payload: Dict[str, Any] = {} + + if nick is not MISSING: + if nick: + payload['nick'] = nick + else: + payload['nick'] = None + + if banner is not MISSING: + if banner: + payload['banner'] = _bytes_to_base64_data(data=banner) + else: + payload['banner'] = None + + if avatar is not MISSING: + if avatar: + payload['avatar'] = _bytes_to_base64_data(data=avatar) + else: + payload['avatar'] = None + + data = await self._state.http.edit_self(guild_id=self.guild.id, **payload) + if payload: + return Member(data=data, guild=self.guild, state=self._state) + async def edit( self, *, From 6fc4fa597eef432b91a71a46c0b8b134340528c9 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:24:40 +0100 Subject: [PATCH 348/354] im dumb --- discord/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index a678034620d2..43c0ac3cb190 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1167,7 +1167,7 @@ def edit_member( return self.request(r, json=fields, reason=reason) def edit_self(self, guild_id: Snowflake, **fields: Any) -> Response[member.MemberWithUser]: - r = Route(method='PATCH', path='/guilds/{guild.id}/members/@me', guild_id_id=guild_id) + r = Route(method='PATCH', path='/guilds/{guild.id}/members/@me', guild_id=guild_id) return self.request(r, json=fields) def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: From 9c331935defdb3e3f529df9615e3c0771831e9fe Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:34:54 +0100 Subject: [PATCH 349/354] im too tired to be coding --- discord/guild.py | 105 +++++++++++++++++++++++++++++++++------------- discord/member.py | 63 ---------------------------- 2 files changed, 77 insertions(+), 91 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 20a50d4e932f..b17c2c79cc25 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1293,8 +1293,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, TextChannelPayload]: - ... + ) -> Coroutine[Any, Any, TextChannelPayload]: ... @overload def _create_channel( @@ -1304,8 +1303,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, VoiceChannelPayload]: - ... + ) -> Coroutine[Any, Any, VoiceChannelPayload]: ... @overload def _create_channel( @@ -1315,8 +1313,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, StageChannelPayload]: - ... + ) -> Coroutine[Any, Any, StageChannelPayload]: ... @overload def _create_channel( @@ -1326,8 +1323,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, CategoryChannelPayload]: - ... + ) -> Coroutine[Any, Any, CategoryChannelPayload]: ... @overload def _create_channel( @@ -1337,8 +1333,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, NewsChannelPayload]: - ... + ) -> Coroutine[Any, Any, NewsChannelPayload]: ... @overload def _create_channel( @@ -1348,8 +1343,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: - ... + ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: ... @overload def _create_channel( @@ -1359,8 +1353,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, ForumChannelPayload]: - ... + ) -> Coroutine[Any, Any, ForumChannelPayload]: ... @overload def _create_channel( @@ -1370,8 +1363,7 @@ def _create_channel( overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, - ) -> Coroutine[Any, Any, GuildChannelPayload]: - ... + ) -> Coroutine[Any, Any, GuildChannelPayload]: ... def _create_channel( self, @@ -1973,6 +1965,69 @@ async def delete(self) -> None: await self._state.http.delete_guild(self.id) + async def edit_me( + self, *, nick: Optional[str] = MISSING, banner: Optional[bytes] = MISSING, avatar: Optional[bytes] = MISSING + ) -> Optional[Member]: + """|coro| + + Edits the current client member profile. + + .. note:: + To upload an avatar, or banner, a :term:`py:bytes-like object` must be passed in that + representes the image being uploaded. If this is done through a file + then the file must be opened via ``open('filename`, 'rb')`` and + the :term:`py:bytes-like object` is given through the use of ``fp.read()``. + + Parameters + ----------- + nick: Optional[:class:`str`] + The nickname you wish to change to. + Could be ``None`` to denote no nick change. + banner: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the image to upload. + Could be ``None`` to denote no avatar. + Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + + Returns + ----------- + Optional[:class:`Member`] + The newly modified client member. + """ + + member = self.me + + # Honestly haven't got a clue if just doing the first one is fine or not. + if not (nick or banner or avatar) or (nick is MISSING and banner is MISSING and avatar is MISSING): + return member + + payload: Dict[str, Any] = {} + + if nick is not MISSING: + if nick: + payload['nick'] = nick + else: + payload['nick'] = None + + if banner is not MISSING: + if banner: + payload['banner'] = utils._bytes_to_base64_data(data=banner) + else: + payload['banner'] = None + + if avatar is not MISSING: + if avatar: + payload['avatar'] = utils._bytes_to_base64_data(data=avatar) + else: + payload['avatar'] = utils._bytes_to_base64_data + + data = await self._state.http.edit_self(guild_id=self.id, **payload) + if payload: + return Member(data=data, guild=self, state=self._state) + async def edit( self, *, @@ -3202,8 +3257,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3218,8 +3272,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3233,8 +3286,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... @overload async def create_scheduled_event( @@ -3248,8 +3300,7 @@ async def create_scheduled_event( description: str = ..., image: bytes = ..., reason: Optional[str] = ..., - ) -> ScheduledEvent: - ... + ) -> ScheduledEvent: ... async def create_scheduled_event( self, @@ -3614,8 +3665,7 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - ) -> Role: - ... + ) -> Role: ... @overload async def create_role( @@ -3628,8 +3678,7 @@ async def create_role( hoist: bool = ..., display_icon: Union[bytes, str] = MISSING, mentionable: bool = ..., - ) -> Role: - ... + ) -> Role: ... async def create_role( self, diff --git a/discord/member.py b/discord/member.py index 0740fa79f8cd..0fdc0cdaf8da 100644 --- a/discord/member.py +++ b/discord/member.py @@ -787,69 +787,6 @@ async def kick(self, *, reason: Optional[str] = None) -> None: """ await self.guild.kick(self, reason=reason) - async def edit_me( - self, *, nick: Optional[str] = MISSING, banner: Optional[bytes] = MISSING, avatar: Optional[bytes] = MISSING - ) -> Optional[Member]: - """|coro| - - Edits the current client member profile. - - .. note:: - To upload an avatar, or banner, a :term:`py:bytes-like object` must be passed in that - representes the image being uploaded. If this is done through a file - then the file must be opened via ``open('filename`, 'rb')`` and - the :term:`py:bytes-like object` is given through the use of ``fp.read()``. - - Parameters - ----------- - nick: Optional[:class:`str`] - The nickname you wish to change to. - Could be ``None`` to denote no nick change. - banner: Optional[:class:`bytes`] - A :term:`py:bytes-like object` representing the image to upload. - Could be ``None`` to denote no avatar. - Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. - avatar: Optional[:class:`bytes`] - A :term:`py:bytes-like object` representing the image to upload. - Could be ``None`` to denote no avatar. - Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. - - Returns - ----------- - Optional[:class:`Member`] - The newly modified client member. - """ - - member = self.guild.me - - # Honestly haven't got a clue if just doing the first one is fine or not. - if not (nick or banner or avatar) or (nick is MISSING and banner is MISSING and avatar is MISSING): - return member - - payload: Dict[str, Any] = {} - - if nick is not MISSING: - if nick: - payload['nick'] = nick - else: - payload['nick'] = None - - if banner is not MISSING: - if banner: - payload['banner'] = _bytes_to_base64_data(data=banner) - else: - payload['banner'] = None - - if avatar is not MISSING: - if avatar: - payload['avatar'] = _bytes_to_base64_data(data=avatar) - else: - payload['avatar'] = None - - data = await self._state.http.edit_self(guild_id=self.guild.id, **payload) - if payload: - return Member(data=data, guild=self.guild, state=self._state) - async def edit( self, *, From 5ffa0b247536777fa1790eb59652b82796204269 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:38:09 +0100 Subject: [PATCH 350/354] Update http.py --- discord/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 43c0ac3cb190..c7f043e7c71e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1167,7 +1167,7 @@ def edit_member( return self.request(r, json=fields, reason=reason) def edit_self(self, guild_id: Snowflake, **fields: Any) -> Response[member.MemberWithUser]: - r = Route(method='PATCH', path='/guilds/{guild.id}/members/@me', guild_id=guild_id) + r = Route(method='PATCH', path='/guilds/{guild_id}/members/@me', guild_id=guild_id) return self.request(r, json=fields) def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: From bf46c7b283988398374fc3ea5528ec064d7d8127 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:40:36 +0100 Subject: [PATCH 351/354] About me --- discord/guild.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index b17c2c79cc25..1f228ff796fe 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1966,7 +1966,12 @@ async def delete(self) -> None: await self._state.http.delete_guild(self.id) async def edit_me( - self, *, nick: Optional[str] = MISSING, banner: Optional[bytes] = MISSING, avatar: Optional[bytes] = MISSING + self, + *, + nick: Optional[str] = MISSING, + banner: Optional[bytes] = MISSING, + avatar: Optional[bytes] = MISSING, + bio: Optional[str] = MISSING, ) -> Optional[Member]: """|coro| @@ -1991,6 +1996,9 @@ async def edit_me( A :term:`py:bytes-like object` representing the image to upload. Could be ``None`` to denote no avatar. Only image formats supported for uploading are JPEG, PNG, GIF, and WEBP. + bio: Optional[:class:`str`] + The about me you wish to change to. + Could be ``None`` to denote no about me change. Returns ----------- @@ -2001,7 +2009,9 @@ async def edit_me( member = self.me # Honestly haven't got a clue if just doing the first one is fine or not. - if not (nick or banner or avatar) or (nick is MISSING and banner is MISSING and avatar is MISSING): + if not (nick or banner or avatar or bio) or ( + nick is MISSING and banner is MISSING and avatar is MISSING or bio is MISSING + ): return member payload: Dict[str, Any] = {} @@ -2024,6 +2034,12 @@ async def edit_me( else: payload['avatar'] = utils._bytes_to_base64_data + if bio is not MISSING: + if bio: + payload['bio'] = bio + else: + payload['bio'] = None + data = await self._state.http.edit_self(guild_id=self.id, **payload) if payload: return Member(data=data, guild=self, state=self._state) From 8cb4c86420b56ecb5b99453fec3dfe948ae16dcd Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:46:51 +0100 Subject: [PATCH 352/354] Update guild.py --- discord/guild.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 1f228ff796fe..f4233e9d40d9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2006,14 +2006,6 @@ async def edit_me( The newly modified client member. """ - member = self.me - - # Honestly haven't got a clue if just doing the first one is fine or not. - if not (nick or banner or avatar or bio) or ( - nick is MISSING and banner is MISSING and avatar is MISSING or bio is MISSING - ): - return member - payload: Dict[str, Any] = {} if nick is not MISSING: From 8f0de540a0e53887d0e74e0635bf349dcb31a29c Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:06:27 +0100 Subject: [PATCH 353/354] Update guild.py --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index f4233e9d40d9..8e7872a2c671 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2024,7 +2024,7 @@ async def edit_me( if avatar: payload['avatar'] = utils._bytes_to_base64_data(data=avatar) else: - payload['avatar'] = utils._bytes_to_base64_data + payload['avatar'] = None if bio is not MISSING: if bio: From 6e8c579da9cdff500ed81f6a805bc1701222e335 Mon Sep 17 00:00:00 2001 From: Samuel <41790962+parelite@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:08:45 +0100 Subject: [PATCH 354/354] ENGLISH!!! --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 8e7872a2c671..2991b89f88a1 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1979,7 +1979,7 @@ async def edit_me( .. note:: To upload an avatar, or banner, a :term:`py:bytes-like object` must be passed in that - representes the image being uploaded. If this is done through a file + represents the image being uploaded. If this is done through a file then the file must be opened via ``open('filename`, 'rb')`` and the :term:`py:bytes-like object` is given through the use of ``fp.read()``.