From 4e276f59d716973a8640c9685eb80b93749f844a Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:53:56 +0200 Subject: [PATCH 01/64] Fix on_socket_raw_send and on_raw_thread_member_remove param types --- docs/api.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index e2f77de97..4a8233065 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -493,6 +493,7 @@ Debug :param payload: The message that is about to be passed on to the WebSocket library. It can be :class:`bytes` to denote a binary message or :class:`str` to denote a regular text message. + :type payload: Union[:class:`bytes`, :class:`str`] Gateway @@ -1363,7 +1364,7 @@ Threads .. versionadded:: 2.0 :param payload: The raw event payload data. - :type member: :class:`RawThreadMembersUpdate` + :type payload: :class:`RawThreadMembersUpdate` Voice ~~~~~~ From 4615d6848aa0fa41ae0578f50f8a57430b344af3 Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:55:18 +0200 Subject: [PATCH 02/64] Add auto_mod_badge to ApplicationFlags --- discord/flags.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/discord/flags.py b/discord/flags.py index 1b0005bd2..d0938583c 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -1422,6 +1422,15 @@ class ApplicationFlags(BaseFlags): rather than using this raw value. """ + @flag_value + def auto_mod_badge(self): + """:class:`bool`: Returns ``True`` if the application uses auto moderation. + This shows up as a badge in the official client. + + .. versionadded:: 2.3 + """ + return 1 << 6 + @flag_value def gateway_presence(self): """:class:`bool`: Returns ``True`` if the application is verified and is allowed to From bee2db805d33456eada62c61cdf11a4c9a01a43d Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:58:50 +0200 Subject: [PATCH 03/64] Add and change Guild attributes and Guild.edit params --- discord/guild.py | 66 +++++++++++++++++++++++++++++++++++++++--- discord/http.py | 1 + discord/types/guild.py | 1 + 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index e86d121ba..67627cfaa 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -267,6 +267,10 @@ class Guild(Hashable): Indicates if the guild has widget enabled. .. versionadded:: 2.0 + max_stage_video_users: Optional[:class:`int`] + The maximum amount of users in a stage video channel. + + .. versionadded:: 2.3 """ __slots__ = ( @@ -315,6 +319,8 @@ class Guild(Hashable): 'approximate_member_count', 'approximate_presence_count', 'premium_progress_bar_enabled', + '_safety_alerts_channel_id', + 'max_stage_video_users', ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { @@ -478,6 +484,7 @@ class Guild(Hashable): self.max_presences: Optional[int] = guild.get('max_presences') self.max_members: Optional[int] = guild.get('max_members') self.max_video_channel_users: Optional[int] = guild.get('max_video_channel_users') + self.max_stage_video_users: Optional[int] = guild.get('max_stage_video_channel_users') self.premium_tier: int = guild.get('premium_tier', 0) self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0 self.vanity_url_code: Optional[str] = guild.get('vanity_url_code') @@ -488,6 +495,7 @@ class Guild(Hashable): self._discovery_splash: Optional[str] = guild.get('discovery_splash') self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id') self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id') + self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'safety_alerts_channel_id') self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0)) self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0)) self.approximate_presence_count: Optional[int] = guild.get('approximate_presence_count') @@ -801,6 +809,17 @@ class Guild(Hashable): channel_id = self._public_updates_channel_id return channel_id and self._channels.get(channel_id) # type: ignore + @property + def safety_alerts_channel(self) -> Optional[TextChannel]: + """Optional[:class:`TextChannel`]: Return's the guild's channel used for safety alerts, if set. + + For example, this is used for the raid protection setting. The guild must have the ``COMMUNITY`` feature. + + .. versionadded:: 2.3 + """ + channel_id = self._safety_alerts_channel_id + return channel_id and self._channels.get(channel_id) # type: ignore + @property def widget_channel(self) -> Optional[Union[TextChannel, ForumChannel, VoiceChannel, StageChannel]]: """Optional[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`, :class:`StageChannel`]]: Returns @@ -1820,6 +1839,8 @@ class Guild(Hashable): widget_enabled: bool = MISSING, widget_channel: Optional[Snowflake] = MISSING, mfa_level: MFALevel = MISSING, + raid_alerts_disabled: bool = MISSING, + safety_alerts_channel: TextChannel = MISSING, ) -> Guild: r"""|coro| @@ -1934,6 +1955,18 @@ class Guild(Hashable): reason: Optional[:class:`str`] The reason for editing this guild. Shows up on the audit log. + raid_alerts_disabled: :class:`bool` + Whether the alerts for raid protection should be disabled for the guild. + + .. versionadded:: 2.3 + + safety_alerts_channel: Optional[:class:`TextChannel`] + 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. + + .. versionadded:: 2.3 + Raises ------- Forbidden @@ -1945,9 +1978,9 @@ class Guild(Hashable): PNG or JPG. This is also raised if you are not the owner of the guild and request an ownership transfer. TypeError - The type passed to the ``default_notifications``, ``verification_level``, - ``explicit_content_filter``, ``system_channel_flags``, or ``mfa_level`` parameter was - of the incorrect type. + 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. Returns -------- @@ -2019,14 +2052,33 @@ class Guild(Hashable): if rules_channel is None: fields['rules_channel_id'] = rules_channel else: + if not isinstance(rules_channel, TextChannel): + raise TypeError(f'rules_channel must be of type TextChannel not {rules_channel.__class__.__name__}') + fields['rules_channel_id'] = rules_channel.id if public_updates_channel is not MISSING: if public_updates_channel is None: fields['public_updates_channel_id'] = public_updates_channel else: + if not isinstance(public_updates_channel, TextChannel): + raise TypeError( + f'public_updates_channel must be of type TextChannel not {public_updates_channel.__class__.__name__}' + ) + fields['public_updates_channel_id'] = public_updates_channel.id + if safety_alerts_channel is not MISSING: + if safety_alerts_channel is None: + fields['safety_alerts_channel_id'] = safety_alerts_channel + else: + if not isinstance(safety_alerts_channel, TextChannel): + raise TypeError( + f'safety_alerts_channel must be of type TextChannel not {safety_alerts_channel.__class__.__name__}' + ) + + fields['safety_alerts_channel_id'] = safety_alerts_channel.id + if owner is not MISSING: if self.owner_id != self._state.self_id: raise ValueError('To transfer ownership you must be the owner of the guild.') @@ -2051,7 +2103,7 @@ class Guild(Hashable): fields['system_channel_flags'] = system_channel_flags.value - if any(feat is not MISSING for feat in (community, discoverable, invites_disabled)): + if any(feat is not MISSING for feat in (community, discoverable, invites_disabled, raid_alerts_disabled)): features = set(self.features) if community is not MISSING: @@ -2077,6 +2129,12 @@ class Guild(Hashable): else: features.discard('INVITES_DISABLED') + if raid_alerts_disabled is not MISSING: + if raid_alerts_disabled: + features.add('RAID_ALERTS_DISABLED') + else: + features.discard('RAID_ALERTS_DISABLED') + fields['features'] = list(features) if premium_progress_bar_enabled is not MISSING: diff --git a/discord/http.py b/discord/http.py index 22336b323..e7c23162a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1426,6 +1426,7 @@ class HTTPClient: 'public_updates_channel_id', 'preferred_locale', 'premium_progress_bar_enabled', + 'safety_alerts_channel_id', ) payload = {k: v for k, v in fields.items() if k in valid_keys} diff --git a/discord/types/guild.py b/discord/types/guild.py index 1ff2854aa..44d51019a 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -84,6 +84,7 @@ GuildFeature = Literal[ 'VERIFIED', 'VIP_REGIONS', 'WELCOME_SCREEN_ENABLED', + 'RAID_ALERTS_DISABLED', ] From 1767be0081070501b2a4e5d1a55b4386ea24bc89 Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Thu, 20 Apr 2023 06:08:04 +0200 Subject: [PATCH 04/64] Add support for voice messages Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/flags.py | 8 ++++++++ discord/message.py | 18 ++++++++++++++++++ discord/permissions.py | 16 ++++++++++++++-- discord/types/message.py | 2 ++ discord/utils.py | 6 +++++- 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/discord/flags.py b/discord/flags.py index d0938583c..837aefc42 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -451,6 +451,14 @@ class MessageFlags(BaseFlags): """ return 4096 + @flag_value + def voice(self): + """:class:`bool`: Returns ``True`` if the message is a voice message. + + .. versionadded:: 2.3 + """ + return 8192 + @fill_with_flags() class PublicUserFlags(BaseFlags): diff --git a/discord/message.py b/discord/message.py index c96c7e022..f99fa01fb 100644 --- a/discord/message.py +++ b/discord/message.py @@ -183,6 +183,14 @@ class Attachment(Hashable): 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 """ __slots__ = ( @@ -197,6 +205,8 @@ class Attachment(Hashable): 'content_type', 'description', 'ephemeral', + 'duration', + 'waveform', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -211,11 +221,19 @@ class Attachment(Hashable): 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') + + waveform = data.get('waveform') + self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None 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'' diff --git a/discord/permissions.py b/discord/permissions.py index 9716f07c6..6be0674c9 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -177,7 +177,7 @@ class Permissions(BaseFlags): """A factory method that creates a :class:`Permissions` with all permissions set to ``True``. """ - return cls(0b1111111111111111111111111111111111111111111111) + return cls(0b11111111111111111111111111111111111111111111111) @classmethod def _timeout_mask(cls) -> int: @@ -261,8 +261,11 @@ class Permissions(BaseFlags): .. versionchanged:: 2.0 Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`, :attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions. + + .. versionchanged:: 2.3 + Added :attr:`send_voice_messages` permission. """ - return cls(0b111110010000000000001111111100001000000) + return cls(0b10000000111110010000000000001111111100001000000) @classmethod def voice(cls) -> Self: @@ -671,6 +674,14 @@ class Permissions(BaseFlags): """ return 1 << 45 + @flag_value + def send_voice_messages(self) -> int: + """:class:`bool`: Returns ``True`` if a user can send voice messages. + + .. versionadded:: 2.3 + """ + return 1 << 46 + def _augment_from_permissions(cls): cls.VALID_NAMES = set(Permissions.VALID_FLAGS) @@ -788,6 +799,7 @@ class PermissionOverwrite: moderate_members: Optional[bool] use_soundboard: Optional[bool] use_external_sounds: Optional[bool] + send_voice_messages: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} diff --git a/discord/types/message.py b/discord/types/message.py index 1319b2e65..1157100fc 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -68,6 +68,8 @@ class Attachment(TypedDict): content_type: NotRequired[str] spoiler: NotRequired[bool] ephemeral: NotRequired[bool] + duration_secs: NotRequired[float] + waveform: NotRequired[str] MessageActivityType = Literal[1, 2, 3, 5] diff --git a/discord/utils.py b/discord/utils.py index fd711387b..9fe664caf 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -56,7 +56,7 @@ from typing import ( TYPE_CHECKING, ) import unicodedata -from base64 import b64encode +from base64 import b64encode, b64decode from bisect import bisect_left import datetime import functools @@ -628,6 +628,10 @@ def _bytes_to_base64_data(data: bytes) -> str: return fmt.format(mime=mime, data=b64) +def _base64_to_bytes(data: str) -> bytes: + return b64decode(data.encode('ascii')) + + def _is_submodule(parent: str, child: str) -> bool: return parent == child or child.startswith(parent + '.') From c6162cfec5534416fe3281fc987bbbf8d9ae281b Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Sat, 22 Apr 2023 02:28:07 +0200 Subject: [PATCH 05/64] Update auto_mod_badge documentation --- discord/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/flags.py b/discord/flags.py index 837aefc42..841a9863f 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -1432,7 +1432,7 @@ class ApplicationFlags(BaseFlags): @flag_value def auto_mod_badge(self): - """:class:`bool`: Returns ``True`` if the application uses auto moderation. + """:class:`bool`: Returns ``True`` if the application uses at least 100 automod rules across all guilds. This shows up as a badge in the official client. .. versionadded:: 2.3 From c57d488ca699de69a240ae7c8304bd53da23141d Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 25 Apr 2023 00:19:20 -0400 Subject: [PATCH 06/64] Fix FileHandler having ANSI escapes when used within PyCharm --- discord/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/utils.py b/discord/utils.py index 9fe664caf..6d1e2ed17 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1243,11 +1243,12 @@ def is_docker() -> bool: def stream_supports_colour(stream: Any) -> bool: + is_a_tty = hasattr(stream, 'isatty') and stream.isatty() + # Pycharm and Vscode support colour in their inbuilt editors if 'PYCHARM_HOSTED' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode': - return True + return is_a_tty - is_a_tty = hasattr(stream, 'isatty') and stream.isatty() if sys.platform != 'win32': # Docker does not consistently have a tty attached to it return is_a_tty or is_docker() From da10065c1912996ff69ccd21602828edf843f305 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 27 Apr 2023 18:05:20 -0400 Subject: [PATCH 07/64] Fix channel edits on webhooks sending two requests --- discord/webhook/async_.py | 3 +-- discord/webhook/sync.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index f9b03193a..b8ff4993e 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1511,8 +1511,7 @@ class Webhook(BaseWebhook): proxy_auth=self.proxy_auth, reason=reason, ) - - if prefer_auth and self.auth_token: + elif prefer_auth and self.auth_token: data = await adapter.edit_webhook( self.id, self.auth_token, diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 71564a58d..982f94a93 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -835,8 +835,7 @@ class SyncWebhook(BaseWebhook): payload['channel_id'] = channel.id data = adapter.edit_webhook(self.id, self.auth_token, payload=payload, session=self.session, reason=reason) - - if prefer_auth and self.auth_token: + elif prefer_auth and self.auth_token: data = adapter.edit_webhook(self.id, self.auth_token, payload=payload, session=self.session, reason=reason) elif self.token: data = adapter.edit_webhook_with_token(self.id, self.token, payload=payload, session=self.session, reason=reason) From 2880f15287cac0e455a0b623532dfe61711981e1 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 1 May 2023 18:14:06 -0400 Subject: [PATCH 08/64] Add changelog for v2.2.3 --- docs/whats_new.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 760be0152..af13da1c1 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. +.. _vp2p2p3: + +v2.2.3 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix crash from Discord sending null ``channel_id`` for automod audit logs. +- Fix ``channel`` edits when using :meth:`Webhook.edit` sending two requests. +- Fix :attr:`AuditLogEntry.target` being ``None`` for invites (:issue:`9336`). +- Fix :exc:`KeyError` when accessing data for :class:`GuildSticker` (:issue:`9324`). + + .. _vp2p2p2: v2.2.2 From c8e26444974f9064f2b4025b2cf6a81753c4172a Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 2 May 2023 20:10:39 -0400 Subject: [PATCH 09/64] Fix custom attributes not being copied over for subclassed Group --- discord/app_commands/commands.py | 33 +++--------------------- tests/test_app_commands_group.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index fd6388443..8e6693408 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -46,6 +46,7 @@ from typing import ( ) import re +from copy import copy as shallow_copy from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale from .models import Choice @@ -707,25 +708,10 @@ class Command(Generic[GroupT, P, T]): ) -> Command: bindings = {} if bindings is MISSING else bindings - cls = self.__class__ - copy = cls.__new__(cls) - copy.name = self.name - copy._locale_name = self._locale_name - copy._guild_ids = self._guild_ids - copy.checks = self.checks - copy.description = self.description - copy._locale_description = self._locale_description - copy.default_permissions = self.default_permissions - copy.guild_only = self.guild_only - copy.nsfw = self.nsfw - copy._attr = self._attr - copy._callback = self._callback - copy.on_error = self.on_error + copy = shallow_copy(self) copy._params = self._params.copy() - copy.module = self.module copy.parent = parent copy.binding = bindings.get(self.binding) if self.binding is not None else binding - copy.extras = self.extras if copy._attr and set_on_binding: setattr(copy.binding, copy._attr, copy) @@ -1622,22 +1608,9 @@ class Group: ) -> Group: bindings = {} if bindings is MISSING else bindings - cls = self.__class__ - copy = cls.__new__(cls) - copy.name = self.name - copy._locale_name = self._locale_name - copy._guild_ids = self._guild_ids - copy.description = self.description - copy._locale_description = self._locale_description + copy = shallow_copy(self) copy.parent = parent - copy.module = self.module - copy.default_permissions = self.default_permissions - copy.guild_only = self.guild_only - copy.nsfw = self.nsfw - copy._attr = self._attr - copy._owner_cls = self._owner_cls copy._children = {} - copy.extras = self.extras bindings[self] = copy diff --git a/tests/test_app_commands_group.py b/tests/test_app_commands_group.py index 756f7c48f..d5f07976f 100644 --- a/tests/test_app_commands_group.py +++ b/tests/test_app_commands_group.py @@ -354,3 +354,46 @@ def test_cog_group_with_subclassed_subclass_group(): assert cog.sub_group.my_command.parent is cog.sub_group assert cog.my_cog_command.parent is cog.sub_group assert cog.my_cog_command.binding is cog + + +def test_cog_group_with_custom_state_issue9383(): + class InnerGroup(app_commands.Group): + def __init__(self): + super().__init__() + self.state: int = 20 + + @app_commands.command() + async def my_command(self, interaction: discord.Interaction) -> None: + ... + + class MyCog(commands.GroupCog): + inner = InnerGroup() + + @app_commands.command() + async def my_regular_command(self, interaction: discord.Interaction) -> None: + ... + + @inner.command() + async def my_inner_command(self, interaction: discord.Interaction) -> None: + ... + + cog = MyCog() + assert cog.inner.state == 20 + assert cog.my_regular_command is not MyCog.my_regular_command + + # Basically the same tests as above... (superset?) + assert MyCog.__cog_app_commands__[0].parent is not cog + assert MyCog.__cog_app_commands__[0].parent is not cog.__cog_app_commands_group__ + assert InnerGroup.__discord_app_commands_group_children__[0].parent is not cog.inner + assert InnerGroup.__discord_app_commands_group_children__[0].parent is not cog.inner + assert cog.inner is not MyCog.inner + assert cog.inner.my_command is not InnerGroup.my_command + assert cog.inner.my_command is not InnerGroup.my_command + assert cog.my_inner_command is not MyCog.my_inner_command + assert not hasattr(cog.inner, 'my_inner_command') + assert cog.__cog_app_commands_group__ is not None + assert cog.__cog_app_commands_group__.parent is None + assert cog.inner.parent is cog.__cog_app_commands_group__ + assert cog.inner.my_command.parent is cog.inner + assert cog.my_inner_command.parent is cog.inner + assert cog.my_inner_command.binding is cog From 34a434b2afb605fb3cb7884608394c258b6ef962 Mon Sep 17 00:00:00 2001 From: Lilly Rose Berner Date: Wed, 3 May 2023 02:19:21 +0200 Subject: [PATCH 10/64] Fix AutoMod audit log entry error due to empty channel_id --- discord/audit_logs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 17f308c45..eebcecfbd 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -652,7 +652,9 @@ class AuditLogEntry(Hashable): ): channel_id = utils._get_as_snowflake(extra, 'channel_id') channel = None - if channel_id is not None: + + # May be an empty string instead of None due to a Discord issue + if channel_id: channel = self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id) self.extra = _AuditLogProxyAutoModAction( From 252ac38f9206e412aa7353c0832fe13951f2ce8e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 5 May 2023 10:55:23 -0400 Subject: [PATCH 11/64] Add fallback ClientUser.mutual_guilds property Fixes #9387 --- discord/user.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord/user.py b/discord/user.py index 23868712c..1b899883a 100644 --- a/discord/user.py +++ b/discord/user.py @@ -409,6 +409,18 @@ class ClientUser(BaseUser): data: UserPayload = await self._state.http.edit_profile(payload) return ClientUser(state=self._state, data=data) + @property + def mutual_guilds(self) -> List[Guild]: + """List[:class:`Guild`]: The guilds that the user shares with the client. + + .. note:: + + This will only return mutual guilds within the client's internal cache. + + .. versionadded:: 1.7 + """ + return list(self._state.guilds) + class User(BaseUser, discord.abc.Messageable): """Represents a Discord user. From 23352fba7949a491257cc06d2d88377035f0a39f Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 6 May 2023 19:20:37 +1000 Subject: [PATCH 12/64] Fix exception raised by around strategy when the limit is set to 100/101 --- discord/abc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index c6b63920f..4fd92e121 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1722,12 +1722,12 @@ class Messageable: async def _around_strategy(retrieve: int, around: Optional[Snowflake], limit: Optional[int]): if not around: - return [] + return [], None, 0 around_id = around.id if around else None data = await self._state.http.logs_from(channel.id, retrieve, around=around_id) - return data, None, limit + return data, None, 0 async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]): after_id = after.id if after else None From 16f1760dd08a91649f594799fff4c39bdf52c0ac Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Sun, 7 May 2023 15:22:38 +0200 Subject: [PATCH 13/64] Document user_limit argument in StageChannel.edit() --- discord/channel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/channel.py b/discord/channel.py index cd5490b2e..907d0b4bd 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -1687,6 +1687,7 @@ class StageChannel(VocalGuildChannel): *, name: str = ..., nsfw: bool = ..., + user_limit: int = ..., position: int = ..., sync_permissions: int = ..., category: Optional[CategoryChannel] = ..., @@ -1726,6 +1727,8 @@ class StageChannel(VocalGuildChannel): The new channel's position. nsfw: :class:`bool` To mark the channel as NSFW or not. + user_limit: :class:`int` + The new channel's user limit. sync_permissions: :class:`bool` Whether to sync permissions with the channel's new or pre-existing category. Defaults to ``False``. From 8d047d9e1d265505b2f93f91669a5074227ea999 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 11 May 2023 02:27:01 -0400 Subject: [PATCH 14/64] Fix opus.Decoder triggering a segfault --- discord/opus.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/opus.py b/discord/opus.py index 7cbdd8386..966a8ccc6 100644 --- a/discord/opus.py +++ b/discord/opus.py @@ -454,7 +454,9 @@ class Decoder(_OpusStruct): channel_count = self.CHANNELS else: frames = self.packet_get_nb_frames(data) - channel_count = self.packet_get_nb_channels(data) + # Discord silent frames erroneously present themselves as 1 channel instead of 2 + # Therefore we need to hardcode the number instead of using packet_get_nb_channels + channel_count = self.CHANNELS samples_per_frame = self.packet_get_samples_per_frame(data) frame_size = frames * samples_per_frame From ca0b44404743eb9e37c5123a9937d561daa63e2a Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Sat, 13 May 2023 20:13:55 +0200 Subject: [PATCH 15/64] Document the change in behaviour of bool(flag_obj) --- discord/flags.py | 46 ++++++++++++++++++++++++++++++++++++++++++ discord/permissions.py | 6 ++++++ docs/migrating.rst | 14 +++++++++++++ 3 files changed, 66 insertions(+) diff --git a/discord/flags.py b/discord/flags.py index 841a9863f..b93a643e5 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -239,6 +239,12 @@ class SystemChannelFlags(BaseFlags): Returns an iterator of ``(name, value)`` pairs. This allows it to be, for example, constructed as a dict or a list of pairs. + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + .. versionadded:: 2.0 + Attributes ----------- value: :class:`int` @@ -361,6 +367,12 @@ class MessageFlags(BaseFlags): Returns an iterator of ``(name, value)`` pairs. This allows it to be, for example, constructed as a dict or a list of pairs. + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + .. versionadded:: 2.0 + .. versionadded:: 1.3 Attributes @@ -509,6 +521,12 @@ class PublicUserFlags(BaseFlags): 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``. + + .. versionadded:: 2.0 + .. versionadded:: 1.4 Attributes @@ -693,6 +711,12 @@ class Intents(BaseFlags): Returns an iterator of ``(name, value)`` pairs. This allows it to be, for example, constructed as a dict or a list of pairs. + .. describe:: bool(b) + + Returns whether any intent is enabled. + + .. versionadded:: 2.0 + Attributes ----------- value: :class:`int` @@ -1278,6 +1302,12 @@ class MemberCacheFlags(BaseFlags): Returns an iterator of ``(name, value)`` pairs. This allows it to be, for example, constructed as a dict or a list of pairs. + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + .. versionadded:: 2.0 + Attributes ----------- value: :class:`int` @@ -1421,6 +1451,10 @@ class ApplicationFlags(BaseFlags): 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``. + .. versionadded:: 2.0 Attributes @@ -1556,6 +1590,10 @@ class ChannelFlags(BaseFlags): 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``. + .. versionadded:: 2.0 Attributes @@ -1652,6 +1690,10 @@ class AutoModPresets(ArrayFlags): 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` @@ -1736,6 +1778,10 @@ class MemberFlags(BaseFlags): 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 ----------- diff --git a/discord/permissions.py b/discord/permissions.py index 6be0674c9..47c50c35f 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -119,6 +119,12 @@ class Permissions(BaseFlags): to be, for example, constructed as a dict or a list of pairs. Note that aliases are not shown. + .. describe:: bool(b) + + Returns whether the permissions object has any permissions set to ``True``. + + .. versionadded:: 2.0 + Attributes ----------- value: :class:`int` diff --git a/docs/migrating.rst b/docs/migrating.rst index 8e31417fd..1a0b52158 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -1006,6 +1006,20 @@ Due to a breaking API change by Discord, :meth:`Guild.bans` no longer returns a async for ban in guild.bans(limit=1000): ... +Flag classes now have a custom ``bool()`` implementation +-------------------------------------------------------- + +To allow library users to easily check whether an instance of a flag class has any flags enabled, +using `bool` on them will now only return ``True`` if at least one flag is enabled. + +This means that evaluating instances of the following classes in a bool context (such as ``if obj:``) may no longer return ``True``: + +- :class:`Intents` +- :class:`MemberCacheFlags` +- :class:`MessageFlags` +- :class:`Permissions` +- :class:`PublicUserFlags` +- :class:`SystemChannelFlags` Function Signature Changes ---------------------------- From be861e5eac826fbea4254853f3944e4a55ab4e07 Mon Sep 17 00:00:00 2001 From: Joab Date: Sat, 13 May 2023 15:15:45 -0300 Subject: [PATCH 16/64] Fix indentation in create_text_channel docs --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 67627cfaa..009a1ef97 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1322,7 +1322,7 @@ class Guild(Hashable): nsfw: :class:`bool` To mark the channel as NSFW or not. news: :class:`bool` - Whether to create the text channel as a news channel. + Whether to create the text channel as a news channel. .. versionadded:: 2.0 default_auto_archive_duration: :class:`int` From 9717317c780d00b65354b59f9397f98513dfc5dc Mon Sep 17 00:00:00 2001 From: Murtada Altarouti <63660298+maltarouti@users.noreply.github.com> Date: Sat, 13 May 2023 21:16:52 +0300 Subject: [PATCH 17/64] Add missing import to modal example --- discord/ui/modal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 615930d5c..b26fa9335 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -64,6 +64,7 @@ class Modal(View): .. code-block:: python3 + import discord from discord import ui class Questionnaire(ui.Modal, title='Questionnaire Response'): From 3951b61440a9fb87255d00349621e7a8863d5856 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 19 May 2023 09:43:51 -0400 Subject: [PATCH 18/64] Allow Interaction webhook URLs to be used in Webhook.from_url --- 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 b8ff4993e..c574914a6 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1275,7 +1275,7 @@ class Webhook(BaseWebhook): A partial :class:`Webhook`. A partial webhook is just a webhook object with an ID and a token. """ - m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P[0-9]{17,20})/(?P[A-Za-z0-9\.\-\_]{60,68})', url) + m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P[0-9]{17,20})/(?P[A-Za-z0-9\.\-\_]{60,})', url) if m is None: raise ValueError('Invalid webhook URL given.') diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index 982f94a93..7da6ada70 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -682,7 +682,7 @@ class SyncWebhook(BaseWebhook): A partial :class:`SyncWebhook`. A partial :class:`SyncWebhook` is just a :class:`SyncWebhook` object with an ID and a token. """ - m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P[0-9]{17,20})/(?P[A-Za-z0-9\.\-\_]{60,68})', url) + m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P[0-9]{17,20})/(?P[A-Za-z0-9\.\-\_]{60,})', url) if m is None: raise ValueError('Invalid webhook URL given.') From be71383af9612f17926f20a503157c8a4cc1b10e Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Sat, 20 May 2023 02:31:39 +0200 Subject: [PATCH 19/64] Add create_expressions permission --- discord/permissions.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/discord/permissions.py b/discord/permissions.py index 47c50c35f..c0b5ee3f4 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -219,6 +219,7 @@ class Permissions(BaseFlags): - :attr:`kick_members` - :attr:`ban_members` - :attr:`administrator` + - :attr:`create_expressions` .. versionchanged:: 1.7 Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_application_commands` permissions. @@ -229,9 +230,9 @@ class Permissions(BaseFlags): :attr:`request_to_speak` permissions. .. versionchanged:: 2.3 - Added :attr:`use_soundboard` + Added :attr:`use_soundboard`, :attr:`create_expressions` permissions. """ - return cls(0b1000111110110110011111101111111111101010001) + return cls(0b01000111110110110011111101111111111101010001) @classmethod def general(cls) -> Self: @@ -243,8 +244,11 @@ class Permissions(BaseFlags): permissions :attr:`administrator`, :attr:`create_instant_invite`, :attr:`kick_members`, :attr:`ban_members`, :attr:`change_nickname` and :attr:`manage_nicknames` are no longer part of the general permissions. + + .. versionchanged:: 2.3 + Added :attr:`create_expressions` permission. """ - return cls(0b01110000000010000000010010110000) + return cls(0b10000000000001110000000010000000010010110000) @classmethod def membership(cls) -> Self: @@ -557,7 +561,7 @@ class Permissions(BaseFlags): @flag_value def manage_guild_expressions(self) -> int: - """:class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis, stickers, and soundboard sounds. + """:class:`bool`: Returns ``True`` if a user can edit or delete emojis, stickers, and soundboard sounds. .. versionadded:: 2.3 """ @@ -672,6 +676,14 @@ class Permissions(BaseFlags): """ return 1 << 42 + @flag_value + def create_expressions(self) -> int: + """:class:`bool`: Returns ``True`` if a user can create emojis, stickers, and soundboard sounds. + + .. versionadded:: 2.3 + """ + return 1 << 43 + @flag_value def use_external_sounds(self) -> int: """:class:`bool`: Returns ``True`` if a user can use sounds from other guilds. @@ -806,6 +818,7 @@ class PermissionOverwrite: use_soundboard: Optional[bool] use_external_sounds: Optional[bool] send_voice_messages: Optional[bool] + create_expressions: Optional[bool] def __init__(self, **kwargs: Optional[bool]): self._values: Dict[str, Optional[bool]] = {} From 92d350059bd6273169c4e6a7d315608113335e60 Mon Sep 17 00:00:00 2001 From: Alex Liu <30390986+applebee1558@users.noreply.github.com> Date: Fri, 19 May 2023 17:32:35 -0700 Subject: [PATCH 20/64] Fix build issue on Sphinx v6 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 8fee2cbfa..a5fcc1773 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ autodoc_typehints = 'none' # napoleon_attr_annotations = False extlinks = { - 'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-'), + 'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-%s'), 'ddocs': ('https://discord.com/developers/docs/%s', None), } From 54c62d7ef88f13cdf152d3f27e9f5cc9ee605a76 Mon Sep 17 00:00:00 2001 From: RedGuy12 <61329810+RedGuy12@users.noreply.github.com> Date: Fri, 19 May 2023 19:35:56 -0500 Subject: [PATCH 21/64] Fix system message for channel_icon_change --- discord/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/message.py b/discord/message.py index f99fa01fb..a7c2f695c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2023,7 +2023,7 @@ class Message(PartialMessage, Hashable): return f'{self.author.name} changed the channel name: **{self.content}**' if self.type is MessageType.channel_icon_change: - return f'{self.author.name} changed the channel icon.' + return f'{self.author.name} changed the group icon.' if self.type is MessageType.pins_add: return f'{self.author.name} pinned a message to this channel.' From 1deb5bf82dcc5f18c4724341c150e1d9cb71293c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Sat, 20 May 2023 06:07:03 +0530 Subject: [PATCH 22/64] [commands] Add Context.filesize_limit property --- discord/ext/commands/context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 92a1a6b51..ee34e1275 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -430,6 +430,14 @@ class Context(discord.abc.Messageable, Generic[BotT]): return None return self.command.cog + @property + def filesize_limit(self) -> int: + """:class:`int`: Returns the maximum number of bytes files can have when uploaded to this guild or DM channel associated with this context. + + .. versionadded:: 2.3 + """ + return self.guild.filesize_limit if self.guild is not None else 26214400 + @discord.utils.cached_property def guild(self) -> Optional[Guild]: """Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available.""" From 53ce05b0d01494ef46c165d363ac0cfafa290f14 Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Sat, 20 May 2023 02:40:52 +0200 Subject: [PATCH 23/64] Add Interaction.channel from Discord payload Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/channel.py | 7 +++- discord/interactions.py | 62 ++++++++++++++++++++++------------- discord/types/channel.py | 10 +++++- discord/types/interactions.py | 3 +- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 907d0b4bd..8c212c0cb 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -2890,7 +2890,12 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable): def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload): self._state: ConnectionState = state - self.recipient: Optional[User] = state.store_user(data['recipients'][0]) + self.recipient: Optional[User] = None + + recipients = data.get('recipients') + if recipients is not None: + self.recipient = state.store_user(recipients[0]) + self.me: ClientUser = me self.id: int = int(data['id']) diff --git a/discord/interactions.py b/discord/interactions.py index f9ed7976d..f62a08703 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -25,6 +25,8 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations + +import logging from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union import asyncio import datetime @@ -33,7 +35,7 @@ from . import utils from .enums import try_enum, Locale, InteractionType, InteractionResponseType from .errors import InteractionResponded, HTTPException, ClientException, DiscordException from .flags import MessageFlags -from .channel import PartialMessageable, ChannelType +from .channel import ChannelType from ._types import ClientT from .user import User @@ -44,6 +46,7 @@ from .http import handle_message_parameters from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params from .app_commands.namespace import Namespace from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation +from .channel import _threaded_channel_factory __all__ = ( 'Interaction', @@ -69,12 +72,19 @@ if TYPE_CHECKING: from .ui.view import View from .app_commands.models import Choice, ChoiceT from .ui.modal import Modal - from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel + from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel from .threads import Thread from .app_commands.commands import Command, ContextMenu InteractionChannel = Union[ - VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, Thread, PartialMessageable + VoiceChannel, + StageChannel, + TextChannel, + ForumChannel, + CategoryChannel, + Thread, + DMChannel, + GroupChannel, ] MISSING: Any = utils.MISSING @@ -96,8 +106,10 @@ class Interaction(Generic[ClientT]): The interaction type. guild_id: Optional[:class:`int`] The guild ID the interaction was sent from. - channel_id: Optional[:class:`int`] - The channel ID the interaction was sent from. + channel: Optional[Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`, :class:`Thread`]] + The channel the interaction was sent from. + + Note that due to a Discord limitation, if sent from a DM channel :attr:`~DMChannel.recipient` is ``None``. application_id: :class:`int` The application ID that the interaction was for. user: Union[:class:`User`, :class:`Member`] @@ -128,7 +140,6 @@ class Interaction(Generic[ClientT]): 'id', 'type', 'guild_id', - 'channel_id', 'data', 'application_id', 'message', @@ -148,7 +159,7 @@ class Interaction(Generic[ClientT]): '_original_response', '_cs_response', '_cs_followup', - '_cs_channel', + 'channel', '_cs_namespace', '_cs_command', ) @@ -171,8 +182,24 @@ class Interaction(Generic[ClientT]): self.data: Optional[InteractionData] = data.get('data') self.token: str = data['token'] self.version: int = data['version'] - self.channel_id: Optional[int] = utils._get_as_snowflake(data, 'channel_id') self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') + self.channel: Optional[InteractionChannel] = None + + raw_channel = data.get('channel', {}) + raw_ch_type = raw_channel.get('type') + if raw_ch_type is not None: + factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None + if factory is None: + logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel)) + else: + if ch_type in (ChannelType.group, ChannelType.private): + channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore + else: + guild = self._state._get_or_create_unavailable_guild(self.guild_id) # type: ignore + channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore + + self.channel = channel + self.application_id: int = int(data['application_id']) self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US')) @@ -227,21 +254,10 @@ class Interaction(Generic[ClientT]): """Optional[:class:`Guild`]: The guild the interaction was sent from.""" return self._state and self._state._get_guild(self.guild_id) - @utils.cached_slot_property('_cs_channel') - def channel(self) -> Optional[InteractionChannel]: - """Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]: The channel the interaction was sent from. - - Note that due to a Discord limitation, DM channels are not resolved since there is - no data to complete them. These are :class:`PartialMessageable` instead. - """ - guild = self.guild - channel = guild and guild._resolve_channel(self.channel_id) - if channel is None: - if self.channel_id is not None: - type = ChannelType.text if self.guild_id is not None else ChannelType.private - return PartialMessageable(state=self._state, guild_id=self.guild_id, id=self.channel_id, type=type) - return None - return channel + @property + def channel_id(self) -> Optional[int]: + """Optional[:class:`int`]: The ID of the channel the interaction was sent from.""" + return self.channel.id if self.channel is not None else None @property def permissions(self) -> Permissions: diff --git a/discord/types/channel.py b/discord/types/channel.py index 421232b45..3068185f1 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -150,16 +150,24 @@ class ForumChannel(_BaseTextChannel): GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel] -class DMChannel(_BaseChannel): +class _BaseDMChannel(_BaseChannel): type: Literal[1] last_message_id: Optional[Snowflake] + + +class DMChannel(_BaseDMChannel): recipients: List[PartialUser] +class InteractionDMChannel(_BaseDMChannel): + recipients: NotRequired[List[PartialUser]] + + class GroupDMChannel(_BaseChannel): type: Literal[3] icon: Optional[str] owner_id: Snowflake + recipients: List[PartialUser] Channel = Union[GuildChannel, DMChannel, GroupDMChannel] diff --git a/discord/types/interactions.py b/discord/types/interactions.py index cfbaf310e..039203dfa 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -27,7 +27,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union from typing_extensions import NotRequired -from .channel import ChannelTypeWithoutThread, ThreadMetadata +from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel from .threads import ThreadType from .member import Member from .message import Attachment @@ -204,6 +204,7 @@ class _BaseInteraction(TypedDict): version: Literal[1] guild_id: NotRequired[Snowflake] channel_id: NotRequired[Snowflake] + channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel] app_permissions: NotRequired[str] locale: NotRequired[str] guild_locale: NotRequired[str] From 66689e16e8ddf9b2bbbb21d2db238f3027ae706c Mon Sep 17 00:00:00 2001 From: Andrin S <65789180+Puncher1@users.noreply.github.com> Date: Sat, 20 May 2023 02:47:16 +0200 Subject: [PATCH 24/64] Add with_counts param to fetch_guilds --- discord/client.py | 14 +++++++++++--- discord/guild.py | 4 ++-- discord/http.py | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/discord/client.py b/discord/client.py index 298959b21..c4c59e75f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2059,13 +2059,15 @@ class Client: limit: Optional[int] = 200, before: Optional[SnowflakeTime] = None, after: Optional[SnowflakeTime] = None, + with_counts: bool = True, ) -> AsyncIterator[Guild]: """Retrieves an :term:`asynchronous iterator` that enables receiving your guilds. .. note:: Using this, you will only receive :attr:`.Guild.owner`, :attr:`.Guild.icon`, - :attr:`.Guild.id`, and :attr:`.Guild.name` per :class:`.Guild`. + :attr:`.Guild.id`, :attr:`.Guild.name`, :attr:`.Guild.approximate_member_count`, + and :attr:`.Guild.approximate_presence_count` per :class:`.Guild`. .. note:: @@ -2106,6 +2108,12 @@ class Client: Retrieve guilds after this date or object. 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. + with_counts: :class:`bool` + Whether to include count information in the guilds. This fills the + :attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count` + attributes without needing any privileged intents. Defaults to ``True``. + + .. versionadded:: 2.3 Raises ------ @@ -2120,7 +2128,7 @@ class Client: async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]): before_id = before.id if before else None - data = await self.http.get_guilds(retrieve, before=before_id) + data = await self.http.get_guilds(retrieve, before=before_id, with_counts=with_counts) if data: if limit is not None: @@ -2132,7 +2140,7 @@ class Client: async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]): after_id = after.id if after else None - data = await self.http.get_guilds(retrieve, after=after_id) + data = await self.http.get_guilds(retrieve, after=after_id, with_counts=with_counts) if data: if limit is not None: diff --git a/discord/guild.py b/discord/guild.py index 009a1ef97..b79f3bb21 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -250,13 +250,13 @@ class Guild(Hashable): approximate_member_count: Optional[:class:`int`] The approximate number of members in the guild. This is ``None`` unless the guild is obtained - using :meth:`Client.fetch_guild` with ``with_counts=True``. + using :meth:`Client.fetch_guild` or :meth:`Client.fetch_guilds` with ``with_counts=True``. .. versionadded:: 2.0 approximate_presence_count: Optional[:class:`int`] The approximate number of members currently active in the guild. Offline members are excluded. This is ``None`` unless the guild is obtained using - :meth:`Client.fetch_guild` with ``with_counts=True``. + :meth:`Client.fetch_guild` or :meth:`Client.fetch_guilds` with ``with_counts=True``. .. versionchanged:: 2.0 premium_progress_bar_enabled: :class:`bool` diff --git a/discord/http.py b/discord/http.py index e7c23162a..5556d1a56 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1373,9 +1373,11 @@ class HTTPClient: limit: int, before: Optional[Snowflake] = None, after: Optional[Snowflake] = None, + with_counts: bool = True, ) -> Response[List[guild.Guild]]: params: Dict[str, Any] = { 'limit': limit, + 'with_counts': int(with_counts), } if before: From df01db34902cb5f44d28189cc142ae19e040433e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 3 May 2023 23:48:46 -0400 Subject: [PATCH 25/64] Initial support for pomelo migration --- discord/abc.py | 7 +++-- discord/ext/commands/converter.py | 36 ++++++++++--------------- discord/flags.py | 1 + discord/guild.py | 30 +++++---------------- discord/member.py | 19 ++++++------- discord/state.py | 1 + discord/team.py | 10 ++++--- discord/types/user.py | 3 ++- discord/user.py | 44 ++++++++++++++++++++++--------- discord/webhook/async_.py | 8 +++++- discord/widget.py | 14 +++++----- 11 files changed, 93 insertions(+), 80 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 4fd92e121..32d843f63 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -219,7 +219,9 @@ class User(Snowflake, Protocol): name: :class:`str` The user's username. discriminator: :class:`str` - The user's discriminator. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname. bot: :class:`bool` If the user is a bot account. system: :class:`bool` @@ -228,6 +230,7 @@ class User(Snowflake, Protocol): name: str discriminator: str + global_name: Optional[str] bot: bool system: bool @@ -248,7 +251,7 @@ class User(Snowflake, Protocol): @property def default_avatar(self) -> Asset: - """:class:`~discord.Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" + """:class:`~discord.Asset`: Returns the default avatar for a given user.""" raise NotImplementedError @property diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index e5be9d47f..0e4812226 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -186,9 +186,9 @@ class MemberConverter(IDConverter[discord.Member]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name - 5. Lookup by nickname + 3. Lookup by guild nickname + 4. Lookup by global name + 5. Lookup by user name .. versionchanged:: 1.5 Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` @@ -196,17 +196,15 @@ class MemberConverter(IDConverter[discord.Member]): .. versionchanged:: 1.5.1 This converter now lazily fetches members from the gateway and HTTP APIs, optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled. + + .. versionchanged:: 2.3 + This converter lookup strategy has changed due to the removal of discriminators. """ async def query_member_named(self, guild: discord.Guild, argument: str) -> Optional[discord.Member]: cache = guild._state.member_cache_flags.joined - if len(argument) > 5 and argument[-5] == '#': - username, _, discriminator = argument.rpartition('#') - members = await guild.query_members(username, limit=100, cache=cache) - return discord.utils.get(members, name=username, discriminator=discriminator) - else: - members = await guild.query_members(argument, limit=100, cache=cache) - return discord.utils.find(lambda m: m.name == argument or m.nick == argument, members) + members = await guild.query_members(argument, limit=100, cache=cache) + return discord.utils.find(lambda m: m.nick == argument or m.global_name == argument or m.name == argument, members) async def query_member_by_id(self, bot: _Bot, guild: discord.Guild, user_id: int) -> Optional[discord.Member]: ws = bot._get_websocket(shard_id=guild.shard_id) @@ -273,8 +271,8 @@ class UserConverter(IDConverter[discord.User]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name + 3. Lookup by global name + 4. Lookup by user name .. versionchanged:: 1.5 Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` @@ -282,6 +280,9 @@ class UserConverter(IDConverter[discord.User]): .. versionchanged:: 1.6 This converter now lazily fetches users from the HTTP APIs if an ID is passed and it's not available in cache. + + .. versionchanged:: 2.3 + This converter lookup strategy has changed due to the removal of discriminators. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.User: @@ -307,16 +308,7 @@ class UserConverter(IDConverter[discord.User]): # Remove first character arg = arg[1:] - # check for discriminator if it exists, - if len(arg) > 5 and arg[-5] == '#': - discrim = arg[-4:] - name = arg[:-5] - predicate = lambda u: u.name == name and u.discriminator == discrim - result = discord.utils.find(predicate, state._users.values()) - if result is not None: - return result - - predicate = lambda u: u.name == arg + predicate = lambda u: u.global_name == arg or u.name == arg result = discord.utils.find(predicate, state._users.values()) if result is None: diff --git a/discord/flags.py b/discord/flags.py index b93a643e5..0fb60084d 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -816,6 +816,7 @@ class Intents(BaseFlags): - :attr:`User.name` - :attr:`User.avatar` - :attr:`User.discriminator` + - :attr:`User.global_name` For more information go to the :ref:`member intent documentation `. diff --git a/discord/guild.py b/discord/guild.py index b79f3bb21..31649376d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1075,26 +1075,20 @@ class Guild(Hashable): def get_member_named(self, name: str, /) -> Optional[Member]: """Returns the first member found that matches the name provided. - The name can have an optional discriminator argument, e.g. "Jake#0001" - or "Jake" will both do the lookup. However the former will give a more - precise result. Note that the discriminator must have all 4 digits - for this to work. - - If a nickname is passed, then it is looked up via the nickname. Note - however, that a nickname + discriminator combo will not lookup the nickname - but rather the username + discriminator combo due to nickname + discriminator - not being unique. - If no member is found, ``None`` is returned. .. versionchanged:: 2.0 ``name`` parameter is now positional-only. + .. versionchanged:: 2.3 + + ``discriminator`` is no longer used in lookup, due to being removed on Discord. + Parameters ----------- name: :class:`str` - The name of the member to lookup with an optional discriminator. + The name of the member to lookup. Returns -------- @@ -1103,22 +1097,10 @@ class Guild(Hashable): then ``None`` is returned. """ - result = None members = self.members - if len(name) > 5 and name[-5] == '#': - # The 5 length is checking to see if #0000 is in the string, - # as a#0000 has a length of 6, the minimum for a potential - # discriminator lookup. - potential_discriminator = name[-4:] - - # do the actual lookup and return if found - # if it isn't found then we'll do a full name lookup below. - result = utils.get(members, name=name[:-5], discriminator=potential_discriminator) - if result is not None: - return result def pred(m: Member) -> bool: - return m.nick == name or m.name == name + return m.nick == name or m.global_name == name or m.name == name return utils.find(pred, members) diff --git a/discord/member.py b/discord/member.py index ee303f5bc..dad54deb0 100644 --- a/discord/member.py +++ b/discord/member.py @@ -274,7 +274,7 @@ class Member(discord.abc.Messageable, _UserTag): .. describe:: str(x) - Returns the member's name with the discriminator. + Returns the member's name with a ``@``. Attributes ---------- @@ -293,7 +293,7 @@ class Member(discord.abc.Messageable, _UserTag): guild: :class:`Guild` The guild that the member belongs to. nick: Optional[:class:`str`] - The guild specific nickname of the user. + The guild specific nickname of the user. Takes precedence over the global name. pending: :class:`bool` Whether the member is pending member verification. @@ -329,6 +329,7 @@ class Member(discord.abc.Messageable, _UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool created_at: datetime.datetime @@ -368,7 +369,7 @@ class Member(discord.abc.Messageable, _UserTag): def __repr__(self) -> str: return ( - f'' ) @@ -463,12 +464,12 @@ class Member(discord.abc.Messageable, _UserTag): def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user - original = (u.name, u._avatar, u.discriminator, u._public_flags) + original = (u.name, u._avatar, u.global_name, u._public_flags) # These keys seem to always be available - modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0)) + modified = (user['username'], user['avatar'], user.get('global_name'), user.get('public_flags', 0)) if original != modified: to_return = User._copy(self._user) - u.name, u._avatar, u.discriminator, u._public_flags = modified + u.name, u._avatar, u.global_name, u._public_flags = modified # Signal to dispatch on_user_update return to_return, u @@ -581,11 +582,11 @@ class Member(discord.abc.Messageable, _UserTag): def display_name(self) -> str: """:class:`str`: Returns the user's display name. - For regular users this is just their username, but - if they have a guild specific nickname then that + For regular users this is just their global name or their username, + but if they have a guild specific nickname then that is returned instead. """ - return self.nick or self.name + return self.nick or self.global_name or self.name @property def display_avatar(self) -> Asset: diff --git a/discord/state.py b/discord/state.py index f2f2a0a83..c13c93b7a 100644 --- a/discord/state.py +++ b/discord/state.py @@ -356,6 +356,7 @@ class ConnectionState(Generic[ClientT]): return self._users[user_id] except KeyError: user = User(state=self, data=data) + # TODO: with the removal of discrims this becomes a bit annoying if user.discriminator != '0000': self._users[user_id] = user return user diff --git a/discord/team.py b/discord/team.py index eef2c2d81..6539e07e2 100644 --- a/discord/team.py +++ b/discord/team.py @@ -108,7 +108,7 @@ class TeamMember(BaseUser): .. describe:: str(x) - Returns the team member's name with discriminator. + Returns the team member's name with a ``@``. .. versionadded:: 1.3 @@ -119,7 +119,11 @@ class TeamMember(BaseUser): id: :class:`int` The team member's unique ID. discriminator: :class:`str` - The team member's discriminator. This is given when the username has conflicts. + The team member's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. team: :class:`Team` @@ -139,5 +143,5 @@ class TeamMember(BaseUser): def __repr__(self) -> str: return ( f'<{self.__class__.__name__} id={self.id} name={self.name!r} ' - f'discriminator={self.discriminator!r} membership_state={self.membership_state!r}>' + f'global_name={self.global_name!r} membership_state={self.membership_state!r}>' ) diff --git a/discord/types/user.py b/discord/types/user.py index fba5aef5b..0a17a5459 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -31,6 +31,7 @@ class PartialUser(TypedDict): username: str discriminator: str avatar: Optional[str] + global_name: Optional[str] PremiumType = Literal[0, 1, 2] @@ -40,7 +41,7 @@ class User(PartialUser, total=False): bot: bool system: bool mfa_enabled: bool - local: str + locale: str verified: bool email: Optional[str] flags: int diff --git a/discord/user.py b/discord/user.py index 1b899883a..b58afaa87 100644 --- a/discord/user.py +++ b/discord/user.py @@ -65,6 +65,7 @@ class BaseUser(_UserTag): 'name', 'id', 'discriminator', + 'global_name', '_avatar', '_banner', '_accent_colour', @@ -78,6 +79,7 @@ class BaseUser(_UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool _state: ConnectionState @@ -92,12 +94,12 @@ class BaseUser(_UserTag): def __repr__(self) -> str: return ( - f"" ) def __str__(self) -> str: - return f'{self.name}#{self.discriminator}' + return f'@{self.name}' def __eq__(self, other: object) -> bool: return isinstance(other, _UserTag) and other.id == self.id @@ -112,6 +114,7 @@ class BaseUser(_UserTag): self.name = data['username'] self.id = int(data['id']) self.discriminator = data['discriminator'] + self.global_name = data.get('global_name') self._avatar = data['avatar'] self._banner = data.get('banner', None) self._accent_colour = data.get('accent_color', None) @@ -126,6 +129,7 @@ class BaseUser(_UserTag): self.name = user.name self.id = user.id self.discriminator = user.discriminator + self.global_name = user.global_name self._avatar = user._avatar self._banner = user._banner self._accent_colour = user._accent_colour @@ -141,6 +145,7 @@ class BaseUser(_UserTag): 'id': self.id, 'avatar': self._avatar, 'discriminator': self.discriminator, + 'global_name': self.global_name, 'bot': self.bot, } @@ -162,8 +167,13 @@ class BaseUser(_UserTag): @property def default_avatar(self) -> Asset: - """:class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" - return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar)) + """:class:`Asset`: Returns the default avatar for a given user.""" + if self.discriminator == '0': + avatar_id = self.id % len(DefaultAvatar) + else: + avatar_id = int(self.discriminator) % len(DefaultAvatar) + + return Asset._from_default_avatar(self._state, avatar_id) @property def display_avatar(self) -> Asset: @@ -260,10 +270,12 @@ class BaseUser(_UserTag): def display_name(self) -> str: """:class:`str`: Returns the user's display name. - For regular users this is just their username, but - if they have a guild specific nickname then that + For regular users this is just their global name or their username, + but if they have a guild specific nickname then that is returned instead. """ + if self.global_name: + return self.global_name return self.name def mentioned_in(self, message: Message) -> bool: @@ -305,7 +317,7 @@ class ClientUser(BaseUser): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. Attributes ----------- @@ -314,7 +326,11 @@ class ClientUser(BaseUser): id: :class:`int` The user's unique ID. discriminator: :class:`str` - The user's discriminator. This is given when the username has conflicts. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -343,7 +359,7 @@ class ClientUser(BaseUser): def __repr__(self) -> str: return ( - f'' ) @@ -441,7 +457,7 @@ class User(BaseUser, discord.abc.Messageable): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. Attributes ----------- @@ -450,7 +466,11 @@ class User(BaseUser, discord.abc.Messageable): id: :class:`int` The user's unique ID. discriminator: :class:`str` - The user's discriminator. This is given when the username has conflicts. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -460,7 +480,7 @@ class User(BaseUser, discord.abc.Messageable): __slots__ = ('__weakref__',) def __repr__(self) -> str: - return f'' + return f'' async def _get_channel(self) -> DMChannel: ch = await self.create_dm() diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index c574914a6..f0a649ff8 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1301,7 +1301,13 @@ class Webhook(BaseWebhook): 'name': name, 'channel_id': channel.id, 'guild_id': channel.guild.id, - 'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user._avatar}, + 'user': { + 'username': user.name, + 'discriminator': user.discriminator, + 'global_name': user.global_name, + 'id': user.id, + 'avatar': user._avatar, + }, } state = channel._state diff --git a/discord/widget.py b/discord/widget.py index 2a7c17a21..b2289c2e8 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -121,7 +121,7 @@ class WidgetMember(BaseUser): .. describe:: str(x) - Returns the widget member's ``name#discriminator``. + Returns the widget member's name with a ``@``. Attributes ----------- @@ -130,13 +130,17 @@ class WidgetMember(BaseUser): name: :class:`str` The member's username. discriminator: :class:`str` - The member's discriminator. + The member's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The member's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Whether the member is a bot. status: :class:`Status` The member's status. nick: Optional[:class:`str`] - The member's nickname. + The member's guild-specific nickname. Takes precedence over the global name. avatar: Optional[:class:`str`] The member's avatar hash. activity: Optional[Union[:class:`BaseActivity`, :class:`Spotify`]] @@ -191,9 +195,7 @@ class WidgetMember(BaseUser): self.connected_channel: Optional[WidgetChannel] = connected_channel def __repr__(self) -> str: - return ( - f"" - ) + return f"" @property def display_name(self) -> str: From b8310268b0b335a7fe5597c0fe678c3b9d62233f Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 4 May 2023 20:43:11 -0400 Subject: [PATCH 26/64] Fix default_avatar detection for migrated users --- discord/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/user.py b/discord/user.py index b58afaa87..303a33ea6 100644 --- a/discord/user.py +++ b/discord/user.py @@ -169,7 +169,7 @@ class BaseUser(_UserTag): def default_avatar(self) -> Asset: """:class:`Asset`: Returns the default avatar for a given user.""" if self.discriminator == '0': - avatar_id = self.id % len(DefaultAvatar) + avatar_id = (self.id >> 22) % len(DefaultAvatar) else: avatar_id = int(self.discriminator) % len(DefaultAvatar) From 9ebc13e160cd339f000d80d1d00dc625ab27b9c0 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 4 May 2023 21:56:34 -0400 Subject: [PATCH 27/64] Only use @ prefix in __str__ if the user is migrated --- discord/member.py | 2 +- discord/team.py | 4 ++-- discord/user.py | 8 +++++--- discord/widget.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/discord/member.py b/discord/member.py index dad54deb0..a45fcae43 100644 --- a/discord/member.py +++ b/discord/member.py @@ -274,7 +274,7 @@ class Member(discord.abc.Messageable, _UserTag): .. describe:: str(x) - Returns the member's name with a ``@``. + Returns the member's handle (e.g. ``@user`` or ``user#discriminator``). Attributes ---------- diff --git a/discord/team.py b/discord/team.py index 6539e07e2..89bc2cfa1 100644 --- a/discord/team.py +++ b/discord/team.py @@ -108,7 +108,7 @@ class TeamMember(BaseUser): .. describe:: str(x) - Returns the team member's name with a ``@``. + Returns the team member's handle (e.g. ``@user`` or ``user#discriminator``). .. versionadded:: 1.3 @@ -121,7 +121,7 @@ class TeamMember(BaseUser): discriminator: :class:`str` The team member's discriminator. This is a legacy concept that is no longer used. global_name: Optional[:class:`str`] - The user's global nickname, taking precedence over the username in display. + The team member's global nickname, taking precedence over the username in display. .. versionadded:: 2.3 bot: :class:`bool` diff --git a/discord/user.py b/discord/user.py index 303a33ea6..6b3a041bb 100644 --- a/discord/user.py +++ b/discord/user.py @@ -99,7 +99,9 @@ class BaseUser(_UserTag): ) def __str__(self) -> str: - return f'@{self.name}' + if self.discriminator == '0': + return f'@{self.name}' + return f'{self.name}#{self.discriminator}' def __eq__(self, other: object) -> bool: return isinstance(other, _UserTag) and other.id == self.id @@ -317,7 +319,7 @@ class ClientUser(BaseUser): .. describe:: str(x) - Returns the user's name with a ``@``. + Returns the user's handle (e.g. ``@user`` or ``user#discriminator``). Attributes ----------- @@ -457,7 +459,7 @@ class User(BaseUser, discord.abc.Messageable): .. describe:: str(x) - Returns the user's name with a ``@``. + Returns the user's handle (e.g. ``@user`` or ``user#discriminator``). Attributes ----------- diff --git a/discord/widget.py b/discord/widget.py index b2289c2e8..268173585 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -121,7 +121,7 @@ class WidgetMember(BaseUser): .. describe:: str(x) - Returns the widget member's name with a ``@``. + Returns the widget member's handle (e.g. ``@user`` or ``user#discriminator``). Attributes ----------- From 48cca30ca1bd400310351c942dec49a2da68016e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 6 May 2023 22:43:25 -0400 Subject: [PATCH 28/64] Revert removal of discriminator lookup --- discord/ext/commands/converter.py | 49 +++++++++++++++++++------------ discord/guild.py | 16 ++++++++-- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 0e4812226..4cd8a3533 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -186,9 +186,11 @@ class MemberConverter(IDConverter[discord.Member]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by guild nickname - 4. Lookup by global name - 5. Lookup by user name + 3. Lookup by username#discriminator (deprecated). + 4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator). + 5. Lookup by guild nickname. + 6. Lookup by global name. + 7. Lookup by user name. .. versionchanged:: 1.5 Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` @@ -197,14 +199,23 @@ class MemberConverter(IDConverter[discord.Member]): This converter now lazily fetches members from the gateway and HTTP APIs, optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled. - .. versionchanged:: 2.3 - This converter lookup strategy has changed due to the removal of discriminators. + .. deprecated:: 2.3 + Looking up users by discriminator will be removed in a future version due to + the removal of discriminators in an API change. """ async def query_member_named(self, guild: discord.Guild, argument: str) -> Optional[discord.Member]: cache = guild._state.member_cache_flags.joined - members = await guild.query_members(argument, limit=100, cache=cache) - return discord.utils.find(lambda m: m.nick == argument or m.global_name == argument or m.name == argument, members) + username, _, discriminator = argument.rpartition('#') + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): + lookup = username + predicate = lambda m: m.name == username and m.discriminator == discriminator + else: + lookup = argument + predicate = lambda m: m.nick == argument or m.global_name == argument or m.name == argument + + members = await guild.query_members(lookup, limit=100, cache=cache) + return discord.utils.find(predicate, members) async def query_member_by_id(self, bot: _Bot, guild: discord.Guild, user_id: int) -> Optional[discord.Member]: ws = bot._get_websocket(shard_id=guild.shard_id) @@ -271,8 +282,10 @@ class UserConverter(IDConverter[discord.User]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by global name - 4. Lookup by user name + 3. Lookup by username#discriminator (deprecated). + 4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator). + 5. Lookup by global name. + 6. Lookup by user name. .. versionchanged:: 1.5 Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` @@ -281,8 +294,9 @@ class UserConverter(IDConverter[discord.User]): This converter now lazily fetches users from the HTTP APIs if an ID is passed and it's not available in cache. - .. versionchanged:: 2.3 - This converter lookup strategy has changed due to the removal of discriminators. + .. deprecated:: 2.3 + Looking up users by discriminator will be removed in a future version due to + the removal of discriminators in an API change. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.User: @@ -301,16 +315,13 @@ class UserConverter(IDConverter[discord.User]): return result # type: ignore - arg = argument - - # Remove the '@' character if this is the first character from the argument - if arg[0] == '@': - # Remove first character - arg = arg[1:] + username, _, discriminator = argument.rpartition('#') + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): + predicate = lambda u: u.name == username and u.discriminator == discriminator + else: + predicate = lambda u: u.global_name == argument or u.name == argument - predicate = lambda u: u.global_name == arg or u.name == arg result = discord.utils.find(predicate, state._users.values()) - if result is None: raise UserNotFound(argument) diff --git a/discord/guild.py b/discord/guild.py index 31649376d..ea5d3a967 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1075,15 +1075,23 @@ class Guild(Hashable): def get_member_named(self, name: str, /) -> Optional[Member]: """Returns the first member found that matches the name provided. + The name is looked up in the following order: + + - Username#Discriminator (deprecated) + - Username#0 (deprecated, only gets users that migrated from their discriminator) + - Nickname + - Global name + - Username + If no member is found, ``None`` is returned. .. versionchanged:: 2.0 ``name`` parameter is now positional-only. - .. versionchanged:: 2.3 + .. deprecated:: 2.3 - ``discriminator`` is no longer used in lookup, due to being removed on Discord. + Looking up users via discriminator due to Discord API change. Parameters ----------- @@ -1099,6 +1107,10 @@ class Guild(Hashable): members = self.members + username, _, discriminator = name.rpartition('#') + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): + return utils.find(lambda m: m.name == username and m.discriminator == discriminator, members) + def pred(m: Member) -> bool: return m.nick == name or m.global_name == name or m.name == name From 8d17aa23c958050ae0c04ae0dbfb1b49c1426a6e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 19 May 2023 20:24:23 -0400 Subject: [PATCH 29/64] Remove @ prefix from usernames Discord seemed to have backtracked on this display --- discord/member.py | 2 +- discord/team.py | 2 +- discord/user.py | 6 +++--- discord/widget.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/member.py b/discord/member.py index a45fcae43..b2a819732 100644 --- a/discord/member.py +++ b/discord/member.py @@ -274,7 +274,7 @@ class Member(discord.abc.Messageable, _UserTag): .. describe:: str(x) - Returns the member's handle (e.g. ``@user`` or ``user#discriminator``). + Returns the member's handle (e.g. ``name`` or ``name#discriminator``). Attributes ---------- diff --git a/discord/team.py b/discord/team.py index 89bc2cfa1..6d441078e 100644 --- a/discord/team.py +++ b/discord/team.py @@ -108,7 +108,7 @@ class TeamMember(BaseUser): .. describe:: str(x) - Returns the team member's handle (e.g. ``@user`` or ``user#discriminator``). + Returns the team member's handle (e.g. ``name`` or ``name#discriminator``). .. versionadded:: 1.3 diff --git a/discord/user.py b/discord/user.py index 6b3a041bb..04efd9653 100644 --- a/discord/user.py +++ b/discord/user.py @@ -100,7 +100,7 @@ class BaseUser(_UserTag): def __str__(self) -> str: if self.discriminator == '0': - return f'@{self.name}' + return self.name return f'{self.name}#{self.discriminator}' def __eq__(self, other: object) -> bool: @@ -319,7 +319,7 @@ class ClientUser(BaseUser): .. describe:: str(x) - Returns the user's handle (e.g. ``@user`` or ``user#discriminator``). + Returns the user's handle (e.g. ``name`` or ``name#discriminator``). Attributes ----------- @@ -459,7 +459,7 @@ class User(BaseUser, discord.abc.Messageable): .. describe:: str(x) - Returns the user's handle (e.g. ``@user`` or ``user#discriminator``). + Returns the user's handle (e.g. ``name`` or ``name#discriminator``). Attributes ----------- diff --git a/discord/widget.py b/discord/widget.py index 268173585..822008665 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -121,7 +121,7 @@ class WidgetMember(BaseUser): .. describe:: str(x) - Returns the widget member's handle (e.g. ``@user`` or ``user#discriminator``). + Returns the widget member's handle (e.g. ``name`` or ``name#discriminator``). Attributes ----------- From 8d583ea71bbb13f30b09b10fc65151948012f2cd Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 19 May 2023 20:28:39 -0400 Subject: [PATCH 30/64] Update discriminator again for user_update events --- discord/member.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/discord/member.py b/discord/member.py index b2a819732..04ce645c9 100644 --- a/discord/member.py +++ b/discord/member.py @@ -464,12 +464,18 @@ class Member(discord.abc.Messageable, _UserTag): def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user - original = (u.name, u._avatar, u.global_name, u._public_flags) + original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags) # These keys seem to always be available - modified = (user['username'], user['avatar'], user.get('global_name'), user.get('public_flags', 0)) + modified = ( + user['username'], + user['discriminator'], + user['avatar'], + user.get('global_name'), + user.get('public_flags', 0), + ) if original != modified: to_return = User._copy(self._user) - u.name, u._avatar, u.global_name, u._public_flags = modified + u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified # Signal to dispatch on_user_update return to_return, u From e870bb1335e3f824c83a40df4ea9b17f215fde63 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 19 May 2023 21:21:37 -0400 Subject: [PATCH 31/64] Add constant for the default file size limit --- discord/ext/commands/context.py | 2 +- discord/guild.py | 6 +++--- discord/utils.py | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index ee34e1275..40ef48cf8 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -436,7 +436,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): .. versionadded:: 2.3 """ - return self.guild.filesize_limit if self.guild is not None else 26214400 + return self.guild.filesize_limit if self.guild is not None else discord.utils.DEFAULT_FILE_SIZE_LIMIT_BYTES @discord.utils.cached_property def guild(self) -> Optional[Guild]: diff --git a/discord/guild.py b/discord/guild.py index ea5d3a967..0c50f5aeb 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -324,9 +324,9 @@ class Guild(Hashable): ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { - None: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=26214400), - 0: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=26214400), - 1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=26214400), + None: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES), + 0: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES), + 1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES), 2: _GuildLimit(emoji=150, stickers=30, bitrate=256e3, filesize=52428800), 3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600), } diff --git a/discord/utils.py b/discord/utils.py index 6d1e2ed17..3ee867975 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -100,6 +100,7 @@ __all__ = ( ) DISCORD_EPOCH = 1420070400000 +DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400 class _MissingSentinel: From 0e727d8e6d386738c1b3b587d9c83b2c253c0e1a Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Wed, 24 May 2023 04:12:12 +0530 Subject: [PATCH 32/64] Fix StageChannel.last_message_id always being None --- discord/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/state.py b/discord/state.py index c13c93b7a..7da1a8f15 100644 --- a/discord/state.py +++ b/discord/state.py @@ -615,7 +615,7 @@ class ConnectionState(Generic[ClientT]): if self._messages is not None: self._messages.append(message) # we ensure that the channel is either a TextChannel, VoiceChannel, or Thread - if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread): + if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread, StageChannel): channel.last_message_id = message.id # type: ignore def parse_message_delete(self, data: gw.MessageDeleteEvent) -> None: From 2ab6541715b8ffc97c8f787749c301d2721705ba Mon Sep 17 00:00:00 2001 From: Sebastian Law Date: Tue, 23 May 2023 17:31:49 -0700 Subject: [PATCH 33/64] [commands] Document exceptions being ignored in teardown and cog_unload Co-authored-by: Josh --- discord/ext/commands/cog.py | 2 ++ docs/ext/commands/extensions.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py index 02136bce8..319f85b80 100644 --- a/discord/ext/commands/cog.py +++ b/discord/ext/commands/cog.py @@ -550,6 +550,8 @@ class Cog(metaclass=CogMeta): Subclasses must replace this if they want special unloading behaviour. + Exceptions raised in this method are ignored during extension unloading. + .. versionchanged:: 2.0 This method can now be a :term:`coroutine`. diff --git a/docs/ext/commands/extensions.rst b/docs/ext/commands/extensions.rst index 9351c9702..d03607845 100644 --- a/docs/ext/commands/extensions.rst +++ b/docs/ext/commands/extensions.rst @@ -54,6 +54,8 @@ Cleaning Up Although rare, sometimes an extension needs to clean-up or know when it's being unloaded. For cases like these, there is another entry point named ``teardown`` which is similar to ``setup`` except called when the extension is unloaded. +Exceptions raised in the ``teardown`` function are ignored, and the extension is still unloaded. + .. code-block:: python3 :caption: basic_ext.py From 70b19c869581cc2fccc7f6b724ffccc231919572 Mon Sep 17 00:00:00 2001 From: Senev <91662824+senev3141@users.noreply.github.com> Date: Sun, 28 May 2023 13:04:38 +0200 Subject: [PATCH 34/64] Updated deprecated versioning in .readthedocs.yml --- .readthedocs.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index f67ef5268..68c792379 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,9 @@ version: 2 formats: [] build: - image: latest + os: "ubuntu-22.04" + tools: + python: "3.8" sphinx: configuration: docs/conf.py @@ -10,7 +12,6 @@ sphinx: builder: html python: - version: 3.8 install: - method: pip path: . From a8882f7cb2dff550102d83ee21c08d95ad259e39 Mon Sep 17 00:00:00 2001 From: kairi Date: Mon, 29 May 2023 17:47:59 +0900 Subject: [PATCH 35/64] Fix Piped Audio Input Ends Prematurely Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/player.py b/discord/player.py index 2030442af..38113ebd8 100644 --- a/discord/player.py +++ b/discord/player.py @@ -210,7 +210,8 @@ class FFmpegAudio(AudioSource): # arbitrarily large read size data = source.read(8192) if not data: - self._process.terminate() + if self._stdin is not None: + self._stdin.close() return try: if self._stdin is not None: From a9bd698683f4f8a9d9904a5ea3e6887b9810002c Mon Sep 17 00:00:00 2001 From: Agent-E11 <116777388+Agent-E11@users.noreply.github.com> Date: Wed, 31 May 2023 19:01:19 -0700 Subject: [PATCH 36/64] Add more specific directions (OAuth2 > URL Generator) --- docs/discord.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/discord.rst b/docs/discord.rst index 63485138e..d58ca8fb0 100644 --- a/docs/discord.rst +++ b/docs/discord.rst @@ -61,7 +61,7 @@ If you want to invite your bot you must create an invite URL for it. 1. Make sure you're logged on to the `Discord website `_. 2. Navigate to the `application page `_ 3. Click on your bot's page. -4. Go to the "OAuth2" tab. +4. Go to the "OAuth2 > URL Generator" tab. .. image:: /images/discord_oauth2.png :alt: How the OAuth2 page should look like. From 6b6cbc44ce66bfb09647188a5bdd3737c351409a Mon Sep 17 00:00:00 2001 From: yvqc <88755906+yvqc@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:50:56 +0200 Subject: [PATCH 37/64] Fix TextInput's is_persistent() flow --- discord/ui/select.py | 4 ++-- discord/ui/text_input.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/discord/ui/select.py b/discord/ui/select.py index bcd7b466d..222596075 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -162,10 +162,10 @@ class BaseSelect(Item[V]): @custom_id.setter def custom_id(self, value: str) -> None: if not isinstance(value, str): - raise TypeError('custom_id must be None or str') + raise TypeError('custom_id must be a str') self._underlying.custom_id = value - self._provided_custom_id = value is not None + self._provided_custom_id = True @property def placeholder(self) -> Optional[str]: diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 79ac652b9..95524ef51 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -137,10 +137,11 @@ class TextInput(Item[V]): @custom_id.setter def custom_id(self, value: str) -> None: if not isinstance(value, str): - raise TypeError('custom_id must be None or str') + raise TypeError('custom_id must be a str') self._underlying.custom_id = value - + self._provided_custom_id = True + @property def width(self) -> int: return 5 From 223e95a252c8fe14be8d84ed8f1dc272075dd87e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 4 Jun 2023 07:52:09 -0400 Subject: [PATCH 38/64] Catch TypeError for unhashable annotation types --- discord/app_commands/transformers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index 0816c5b87..8f009181b 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -750,7 +750,7 @@ def get_supported_annotation( try: return (_mapping[annotation], MISSING, True) - except KeyError: + except (KeyError, TypeError): pass if isinstance(annotation, Transformer): From 914773d0fb4c5c66edce4d1f033093b7fe0d6d1b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 4 Jun 2023 07:53:25 -0400 Subject: [PATCH 39/64] Remove trailing whitespace --- discord/ui/text_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ui/text_input.py b/discord/ui/text_input.py index 95524ef51..23c1d874f 100644 --- a/discord/ui/text_input.py +++ b/discord/ui/text_input.py @@ -141,7 +141,7 @@ class TextInput(Item[V]): self._underlying.custom_id = value self._provided_custom_id = True - + @property def width(self) -> int: return 5 From e6d2d82803d79cdd3fccecb7c5fd473e98100787 Mon Sep 17 00:00:00 2001 From: Michael H Date: Sun, 4 Jun 2023 15:11:16 -0700 Subject: [PATCH 40/64] Set socket family of connector to AF_INET - discord doesn't support ipv6 - prevents issues with hosts with DNS64 enabled - resolves #9442 --- discord/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 5556d1a56..6e803830e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -48,6 +48,7 @@ from typing import ( from urllib.parse import quote as _uriquote from collections import deque import datetime +import socket import aiohttp @@ -784,7 +785,8 @@ class HTTPClient: async def static_login(self, token: str) -> user.User: # Necessary to get aiohttp to stop complaining about session creation if self.connector is MISSING: - self.connector = aiohttp.TCPConnector(limit=0) + # discord does not support ipv6 + self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET) self.__session = aiohttp.ClientSession( connector=self.connector, From eedd7e3014220e4237d69e6a1597bde91d6046de Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 7 Jun 2023 22:12:47 -0400 Subject: [PATCH 41/64] Add support for new pink default avatars --- discord/colour.py | 10 ++++++++++ discord/enums.py | 1 + discord/user.py | 2 +- docs/api.rst | 6 ++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/discord/colour.py b/discord/colour.py index 52dca9cc0..e640f9df4 100644 --- a/discord/colour.py +++ b/discord/colour.py @@ -509,5 +509,15 @@ class Colour: """ return cls(0xEEEFF1) + @classmethod + def pink(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xEB459F``. + + .. colour:: #EB459F + + .. versionadded:: 2.3 + """ + return cls(0xEB459F) + Color = Colour diff --git a/discord/enums.py b/discord/enums.py index 94ca8c726..81d5cb444 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -295,6 +295,7 @@ class DefaultAvatar(Enum): green = 2 orange = 3 red = 4 + pink = 5 def __str__(self) -> str: return self.name diff --git a/discord/user.py b/discord/user.py index 04efd9653..cc836374a 100644 --- a/discord/user.py +++ b/discord/user.py @@ -173,7 +173,7 @@ class BaseUser(_UserTag): if self.discriminator == '0': avatar_id = (self.id >> 22) % len(DefaultAvatar) else: - avatar_id = int(self.discriminator) % len(DefaultAvatar) + avatar_id = int(self.discriminator) % 5 return Asset._from_default_avatar(self._state, avatar_id) diff --git a/docs/api.rst b/docs/api.rst index 4a8233065..8a899bbc7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2896,6 +2896,12 @@ of :class:`enum.Enum`. Represents the default avatar with the color red. See also :attr:`Colour.red` + .. attribute:: pink + + Represents the default avatar with the color pink. + See also :attr:`Colour.pink` + + .. versionadded:: 2.3 .. class:: StickerType From d34a88411d3d973453d80128ab924f6aca197995 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 7 Jun 2023 22:13:58 -0400 Subject: [PATCH 42/64] Fix spelling of DefaultAvatar colour doc strings --- docs/api.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8a899bbc7..29bb530ba 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2875,30 +2875,30 @@ of :class:`enum.Enum`. .. attribute:: blurple - Represents the default avatar with the color blurple. + Represents the default avatar with the colour blurple. See also :attr:`Colour.blurple` .. attribute:: grey - Represents the default avatar with the color grey. + Represents the default avatar with the colour grey. See also :attr:`Colour.greyple` .. attribute:: gray An alias for :attr:`grey`. .. attribute:: green - Represents the default avatar with the color green. + Represents the default avatar with the colour green. See also :attr:`Colour.green` .. attribute:: orange - Represents the default avatar with the color orange. + Represents the default avatar with the colour orange. See also :attr:`Colour.orange` .. attribute:: red - Represents the default avatar with the color red. + Represents the default avatar with the colour red. See also :attr:`Colour.red` .. attribute:: pink - Represents the default avatar with the color pink. + Represents the default avatar with the colour pink. See also :attr:`Colour.pink` .. versionadded:: 2.3 From 94bf7d8644954518447f8636d58267259ef27947 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 11 Jun 2023 18:38:39 +0200 Subject: [PATCH 43/64] [commands] Add displayed_name to commands.Parameter --- discord/ext/commands/core.py | 12 ++++++++++-- discord/ext/commands/errors.py | 8 ++++---- discord/ext/commands/help.py | 2 +- discord/ext/commands/parameters.py | 27 ++++++++++++++++++++++++--- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 8140dca00..ffbefe284 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -151,6 +151,7 @@ def get_signature_parameters( parameter._default = default.default parameter._description = default._description parameter._displayed_default = default._displayed_default + parameter._displayed_name = default._displayed_name annotation = parameter.annotation @@ -194,8 +195,13 @@ def extract_descriptions_from_docstring(function: Callable[..., Any], params: Di description, param_docstring = divide for match in NUMPY_DOCSTRING_ARG_REGEX.finditer(param_docstring): name = match.group('name') + if name not in params: - continue + is_display_name = discord.utils.get(params.values(), displayed_name=name) + if is_display_name: + name = is_display_name.name + else: + continue param = params[name] if param.description is None: @@ -1169,7 +1175,9 @@ class Command(_BaseCommand, Generic[CogT, P, T]): return '' result = [] - for name, param in params.items(): + for param in params.values(): + name = param.displayed_name or param.name + greedy = isinstance(param.converter, Greedy) optional = False # postpone evaluation of if it's an optional argument diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index b25e6ae95..736d0b5af 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -182,7 +182,7 @@ class MissingRequiredArgument(UserInputError): def __init__(self, param: Parameter) -> None: self.param: Parameter = param - super().__init__(f'{param.name} is a required argument that is missing.') + super().__init__(f'{param.displayed_name or param.name} is a required argument that is missing.') class MissingRequiredAttachment(UserInputError): @@ -201,7 +201,7 @@ class MissingRequiredAttachment(UserInputError): def __init__(self, param: Parameter) -> None: self.param: Parameter = param - super().__init__(f'{param.name} is a required argument that is missing an attachment.') + super().__init__(f'{param.displayed_name or param.name} is a required argument that is missing an attachment.') class TooManyArguments(UserInputError): @@ -901,7 +901,7 @@ class BadUnionArgument(UserInputError): else: fmt = ' or '.join(to_string) - super().__init__(f'Could not convert "{param.name}" into {fmt}.') + super().__init__(f'Could not convert "{param.displayed_name or param.name}" into {fmt}.') class BadLiteralArgument(UserInputError): @@ -938,7 +938,7 @@ class BadLiteralArgument(UserInputError): else: fmt = ' or '.join(to_string) - super().__init__(f'Could not convert "{param.name}" into the literal {fmt}.') + super().__init__(f'Could not convert "{param.displayed_name or param.name}" into the literal {fmt}.') class ArgumentParsingError(UserInputError): diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index d8f341474..32228d205 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -1166,7 +1166,7 @@ class DefaultHelpCommand(HelpCommand): get_width = discord.utils._string_width for argument in arguments: - name = argument.name + name = argument.displayed_name or argument.name width = max_size - (get_width(name) - len(name)) entry = f'{self.indent * " "}{name:<{width}} {argument.description or self.default_argument_description}' # we do not want to shorten the default value, if any. diff --git a/discord/ext/commands/parameters.py b/discord/ext/commands/parameters.py index 5039a16aa..d3302f5a3 100644 --- a/discord/ext/commands/parameters.py +++ b/discord/ext/commands/parameters.py @@ -87,7 +87,7 @@ class Parameter(inspect.Parameter): .. versionadded:: 2.0 """ - __slots__ = ('_displayed_default', '_description', '_fallback') + __slots__ = ('_displayed_default', '_description', '_fallback', '_displayed_name') def __init__( self, @@ -97,6 +97,7 @@ class Parameter(inspect.Parameter): annotation: Any = empty, description: str = empty, displayed_default: str = empty, + displayed_name: str = empty, ) -> None: super().__init__(name=name, kind=kind, default=default, annotation=annotation) self._name = name @@ -106,6 +107,7 @@ class Parameter(inspect.Parameter): self._annotation = annotation self._displayed_default = displayed_default self._fallback = False + self._displayed_name = displayed_name def replace( self, @@ -116,6 +118,7 @@ class Parameter(inspect.Parameter): annotation: Any = MISSING, description: str = MISSING, displayed_default: Any = MISSING, + displayed_name: Any = MISSING, ) -> Self: if name is MISSING: name = self._name @@ -129,6 +132,8 @@ class Parameter(inspect.Parameter): description = self._description if displayed_default is MISSING: displayed_default = self._displayed_default + if displayed_name is MISSING: + displayed_name = self._displayed_name return self.__class__( name=name, @@ -137,6 +142,7 @@ class Parameter(inspect.Parameter): annotation=annotation, description=description, displayed_default=displayed_default, + displayed_name=displayed_name, ) if not TYPE_CHECKING: # this is to prevent anything breaking if inspect internals change @@ -171,6 +177,14 @@ class Parameter(inspect.Parameter): return None if self.required else str(self.default) + @property + def displayed_name(self) -> Optional[str]: + """Optional[:class:`str`]: The name that is displayed to the user. + + .. versionadded:: 2.3 + """ + return self._displayed_name if self._displayed_name is not empty else None + async def get_default(self, ctx: Context[Any]) -> Any: """|coro| @@ -193,8 +207,9 @@ def parameter( default: Any = empty, description: str = empty, displayed_default: str = empty, + displayed_name: str = empty, ) -> Any: - r"""parameter(\*, converter=..., default=..., description=..., displayed_default=...) + r"""parameter(\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...) A way to assign custom metadata for a :class:`Command`\'s parameter. @@ -221,6 +236,10 @@ def parameter( The description of this parameter. displayed_default: :class:`str` The displayed default in :attr:`Command.signature`. + displayed_name: :class:`str` + The name that is displayed to the user. + + .. versionadded:: 2.3 """ return Parameter( name='empty', @@ -229,6 +248,7 @@ def parameter( default=default, description=description, displayed_default=displayed_default, + displayed_name=displayed_name, ) @@ -240,12 +260,13 @@ class ParameterAlias(Protocol): default: Any = empty, description: str = empty, displayed_default: str = empty, + displayed_name: str = empty, ) -> Any: ... param: ParameterAlias = parameter -r"""param(\*, converter=..., default=..., description=..., displayed_default=...) +r"""param(\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...) An alias for :func:`parameter`. From f1bade4bda43ae95ba457ff381d2927eb27fea1b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Jun 2023 12:21:37 -0400 Subject: [PATCH 44/64] Hoist webhook detection outside of store_user helper --- discord/interactions.py | 4 ++-- discord/message.py | 2 +- discord/state.py | 7 +++---- discord/webhook/async_.py | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index f62a08703..44645396e 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -1041,8 +1041,8 @@ class _InteractionMessageState: def _get_guild(self, guild_id): return self._parent._get_guild(guild_id) - def store_user(self, data): - return self._parent.store_user(data) + def store_user(self, data, *, cache: bool = True): + return self._parent.store_user(data, cache=cache) def create_user(self, data): return self._parent.create_user(data) diff --git a/discord/message.py b/discord/message.py index a7c2f695c..a3e788d0c 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1808,7 +1808,7 @@ class Message(PartialMessage, Hashable): self.nonce = value def _handle_author(self, author: UserPayload) -> None: - self.author = self._state.store_user(author) + self.author = self._state.store_user(author, cache=self.webhook_id is None) if isinstance(self.guild, Guild): found = self.guild.get_member(self.author.id) if found is not None: diff --git a/discord/state.py b/discord/state.py index 7da1a8f15..d509c23fd 100644 --- a/discord/state.py +++ b/discord/state.py @@ -349,19 +349,18 @@ class ConnectionState(Generic[ClientT]): for vc in self.voice_clients: vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime). - def store_user(self, data: Union[UserPayload, PartialUserPayload]) -> User: + def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> User: # this way is 300% faster than `dict.setdefault`. user_id = int(data['id']) try: return self._users[user_id] except KeyError: user = User(state=self, data=data) - # TODO: with the removal of discrims this becomes a bit annoying - if user.discriminator != '0000': + if cache: self._users[user_id] = user return user - def store_user_no_intents(self, data: Union[UserPayload, PartialUserPayload]) -> User: + def store_user_no_intents(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> User: return User(state=self, data=data) def create_user(self, data: Union[UserPayload, PartialUserPayload]) -> User: diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index f0a649ff8..9ce18bbe4 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -715,9 +715,9 @@ class _WebhookState: return self._parent._get_guild(guild_id) return None - def store_user(self, data: Union[UserPayload, PartialUserPayload]) -> BaseUser: + def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser: if self._parent is not None: - return self._parent.store_user(data) + return self._parent.store_user(data, cache=cache) # state parameter is artificial return BaseUser(state=self, data=data) # type: ignore From c5da0fe7c1442981de1aac6bb91ea7046d5cc79f Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Jun 2023 12:29:13 -0400 Subject: [PATCH 45/64] Remove and address stale TODO comments --- discord/abc.py | 2 -- discord/guild.py | 36 +++++++++++++----------------------- discord/message.py | 1 - discord/reaction.py | 1 - 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 32d843f63..242662d15 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -946,8 +946,6 @@ class GuildChannel: if len(permissions) > 0: raise TypeError('Cannot mix overwrite and keyword arguments.') - # TODO: wait for event - if overwrite is None: await http.delete_channel_permissions(self.id, target.id, reason=reason) elif isinstance(overwrite, PermissionOverwrite): diff --git a/discord/guild.py b/discord/guild.py index 0c50f5aeb..b95ac4247 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -502,56 +502,47 @@ class Guild(Hashable): self.approximate_member_count: Optional[int] = guild.get('approximate_member_count') self.premium_progress_bar_enabled: bool = guild.get('premium_progress_bar_enabled', False) self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') - - self._sync(guild) self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250 - self.afk_channel: Optional[VocalGuildChannel] = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) # type: ignore - - # TODO: refactor/remove? - def _sync(self, data: GuildPayload) -> None: - try: - self._large = data['large'] - except KeyError: - pass - - if 'channels' in data: - channels = data['channels'] + if 'channels' in guild: + channels = guild['channels'] for c in channels: factory, ch_type = _guild_channel_factory(c['type']) if factory: self._add_channel(factory(guild=self, data=c, state=self._state)) # type: ignore - for obj in data.get('voice_states', []): + self.afk_channel: Optional[VocalGuildChannel] = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) # type: ignore + + for obj in guild.get('voice_states', []): self._update_voice_state(obj, int(obj['channel_id'])) cache_joined = self._state.member_cache_flags.joined cache_voice = self._state.member_cache_flags.voice self_id = self._state.self_id - for mdata in data.get('members', []): + for mdata in guild.get('members', []): member = Member(data=mdata, guild=self, state=self._state) # type: ignore # Members will have the 'user' key in this scenario if cache_joined or member.id == self_id or (cache_voice and member.id in self._voice_states): self._add_member(member) empty_tuple = () - for presence in data.get('presences', []): + for presence in guild.get('presences', []): user_id = int(presence['user']['id']) member = self.get_member(user_id) if member is not None: member._presence_update(presence, empty_tuple) # type: ignore - if 'threads' in data: - threads = data['threads'] + if 'threads' in guild: + threads = guild['threads'] for thread in threads: self._add_thread(Thread(guild=self, state=self._state, data=thread)) - if 'stage_instances' in data: - for s in data['stage_instances']: + if 'stage_instances' in guild: + for s in guild['stage_instances']: stage_instance = StageInstance(guild=self, data=s, state=self._state) self._stage_instances[stage_instance.id] = stage_instance - if 'guild_scheduled_events' in data: - for s in data['guild_scheduled_events']: + if 'guild_scheduled_events' in guild: + for s in guild['guild_scheduled_events']: scheduled_event = ScheduledEvent(data=s, state=self._state) self._scheduled_events[scheduled_event.id] = scheduled_event @@ -3484,7 +3475,6 @@ class Guild(Hashable): data = await self._state.http.create_role(self.id, reason=reason, **fields) role = Role(guild=self, data=data, state=self._state) - # TODO: add to cache return role async def edit_role_positions(self, positions: Mapping[Snowflake, int], *, reason: Optional[str] = None) -> List[Role]: diff --git a/discord/message.py b/discord/message.py index a3e788d0c..338d9c33e 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1827,7 +1827,6 @@ class Message(PartialMessage, Hashable): author._update_from_message(member) # type: ignore except AttributeError: # It's a user here - # TODO: consider adding to cache here self.author = Member._from_message(message=self, data=member) def _handle_mentions(self, mentions: List[UserWithMemberPayload]) -> None: diff --git a/discord/reaction.py b/discord/reaction.py index 5f50ec8f4..c0cbb8ee5 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -89,7 +89,6 @@ class Reaction: self.count: int = data.get('count', 1) self.me: bool = data['me'] - # TODO: typeguard def is_custom_emoji(self) -> bool: """:class:`bool`: If this is a custom emoji.""" return not isinstance(self.emoji, str) From abfb3a11b056e0a32db93fcae9a907e03279c452 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Jun 2023 12:35:27 -0400 Subject: [PATCH 46/64] Refactor Guild.afk_channel to use a property --- discord/guild.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index b95ac4247..e8c7ebcf0 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -275,7 +275,6 @@ class Guild(Hashable): __slots__ = ( 'afk_timeout', - 'afk_channel', 'name', 'id', 'unavailable', @@ -298,6 +297,7 @@ class Guild(Hashable): 'vanity_url_code', 'widget_enabled', '_widget_channel_id', + '_afk_channel_id', '_members', '_channels', '_icon', @@ -503,6 +503,7 @@ class Guild(Hashable): self.premium_progress_bar_enabled: bool = guild.get('premium_progress_bar_enabled', False) 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') if 'channels' in guild: channels = guild['channels'] @@ -511,8 +512,6 @@ class Guild(Hashable): if factory: self._add_channel(factory(guild=self, data=c, state=self._state)) # type: ignore - self.afk_channel: Optional[VocalGuildChannel] = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) # type: ignore - for obj in guild.get('voice_states', []): self._update_voice_state(obj, int(obj['channel_id'])) @@ -761,6 +760,14 @@ class Guild(Hashable): return emoji return None + @property + def afk_channel(self) -> Optional[VocalGuildChannel]: + """Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]]: The channel that denotes the AFK channel. + + If no channel is set, then this returns ``None``. + """ + return self.get_channel(self._afk_channel_id) # type: ignore + @property def system_channel(self) -> Optional[TextChannel]: """Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages. @@ -1875,8 +1882,6 @@ class Guild(Hashable): and ``public_updates_channel`` parameters are required. .. versionadded:: 2.0 - afk_channel: Optional[:class:`VoiceChannel`] - The new channel that is the AFK channel. Could be ``None`` for no AFK channel. afk_timeout: :class:`int` The number of seconds until someone is moved to the AFK channel. owner: :class:`Member` From a8675ccde88c4d710928ba65c813a4126cbb25f7 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Jun 2023 12:52:47 -0400 Subject: [PATCH 47/64] Fix afk_channel docstring error --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index e8c7ebcf0..a37fb549f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -186,8 +186,6 @@ class Guild(Hashable): .. versionadded:: 2.0 afk_timeout: :class:`int` The number of seconds until someone is moved to the AFK channel. - afk_channel: Optional[:class:`VoiceChannel`] - The channel that denotes the AFK channel. ``None`` if it doesn't exist. id: :class:`int` The guild's ID. owner_id: :class:`int` @@ -1882,6 +1880,8 @@ class Guild(Hashable): and ``public_updates_channel`` parameters are required. .. versionadded:: 2.0 + afk_channel: Optional[:class:`VoiceChannel`] + The new channel that is the AFK channel. Could be ``None`` for no AFK channel. afk_timeout: :class:`int` The number of seconds until someone is moved to the AFK channel. owner: :class:`Member` From cf681105ceb88723b926aef6f41b237fff0b312c Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 11 Jun 2023 13:52:16 -0400 Subject: [PATCH 48/64] Rename Permissions.manage_guild_expressions to manage_expressions --- discord/permissions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/discord/permissions.py b/discord/permissions.py index c0b5ee3f4..74cf22ef4 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -210,7 +210,7 @@ class Permissions(BaseFlags): ``True`` and the guild-specific ones set to ``False``. The guild-specific permissions are currently: - - :attr:`manage_guild_expressions` + - :attr:`manage_expressions` - :attr:`view_audit_log` - :attr:`view_guild_insights` - :attr:`manage_guild` @@ -321,7 +321,7 @@ class Permissions(BaseFlags): - :attr:`manage_messages` - :attr:`manage_roles` - :attr:`manage_webhooks` - - :attr:`manage_guild_expressions` + - :attr:`manage_expressions` - :attr:`manage_threads` - :attr:`moderate_members` @@ -560,21 +560,21 @@ class Permissions(BaseFlags): return 1 << 29 @flag_value - def manage_guild_expressions(self) -> int: + def manage_expressions(self) -> int: """:class:`bool`: Returns ``True`` if a user can edit or delete emojis, stickers, and soundboard sounds. .. versionadded:: 2.3 """ return 1 << 30 - @make_permission_alias('manage_guild_expressions') + @make_permission_alias('manage_expressions') def manage_emojis(self) -> int: - """:class:`bool`: An alias for :attr:`manage_guild_expressions`.""" + """:class:`bool`: An alias for :attr:`manage_expressions`.""" return 1 << 30 - @make_permission_alias('manage_guild_expressions') + @make_permission_alias('manage_expressions') def manage_emojis_and_stickers(self) -> int: - """:class:`bool`: An alias for :attr:`manage_guild_expressions`. + """:class:`bool`: An alias for :attr:`manage_expressions`. .. versionadded:: 2.0 """ @@ -801,7 +801,7 @@ class PermissionOverwrite: manage_roles: Optional[bool] manage_permissions: Optional[bool] manage_webhooks: Optional[bool] - manage_guild_expressions: Optional[bool] + manage_expressions: Optional[bool] manage_emojis: Optional[bool] manage_emojis_and_stickers: Optional[bool] use_application_commands: Optional[bool] From 9f5b6c2bed56a34fd1061c93fa682015e36fb43e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 12 Jun 2023 13:40:12 -0400 Subject: [PATCH 49/64] Add changelog for v2.3 --- docs/whats_new.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index af13da1c1..04effd7de 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,78 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p3p0: + +v2.3.0 +-------- + +New Features +~~~~~~~~~~~~~ + +- Add support for the new username system (also known as "pomelo"). + - Add :attr:`User.global_name` to get their global nickname or "display name". + - Update :attr:`User.display_name` and :attr:`Member.display_name` to understand global nicknames. + - Update ``__str__`` for :class:`User` to drop discriminators if the user has been migrated. + - Update :meth:`Guild.get_member_named` to work with migrated users. + - Update :attr:`User.default_avatar` to work with migrated users. + - |commands| Update user and member converters to understand migrated users. + +- Add :attr:`DefaultAvatar.pink` for new pink default avatars. +- Add :meth:`Colour.pink` to get the pink default avatar colour. +- Add support for voice messages (:issue:`9358`) + - Add :attr:`MessageFlags.voice` + - Add :attr:`Attachment.duration` and :attr:`Attachment.waveform` + - Add :meth:`Attachment.is_voice_message` + - This does not support *sending* voice messages because this is currently unsupported by the API. + +- Add support for new :attr:`Interaction.channel` attribute from the API update (:issue:`9339`). +- Add support for :attr:`TextChannel.default_thread_slowmode_delay` (:issue:`9291`). +- Add support for :attr:`ForumChannel.default_sort_order` (:issue:`9290`). +- Add support for ``default_reaction_emoji`` and ``default_forum_layout`` in :meth:`Guild.create_forum` (:issue:`9300`). +- Add support for ``widget_channel``, ``widget_enabled``, and ``mfa_level`` in :meth:`Guild.edit` (:issue:`9302`, :issue:`9303`). +- Add various new :class:`Permissions` and changes (:issue:`9312`, :issue:`9325`, :issue:`9358`, :issue:`9378`) + - 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. + - Change :attr:`Permissions.manage_emojis` to be an alias of :attr:`~Permissions.manage_expressions`. + +- Add various new properties to :class:`PartialAppInfo` and :class:`AppInfo` (:issue:`9298`). +- Add support for ``with_counts`` parameter to :meth:`Client.fetch_guilds` (:issue:`9369`). +- Add new :meth:`Guild.get_emoji` helper (:issue:`9296`). +- Add :attr:`ApplicationFlags.auto_mod_badge` (:issue:`9313`). +- Add :attr:`Guild.max_stage_video_users` and :attr:`Guild.safety_alerts_channel` (:issue:`9318`). +- Add support for ``raid_alerts_disabled`` and ``safety_alerts_channel`` in :meth:`Guild.edit` (:issue:`9318`). +- |commands| Add :attr:`BadLiteralArgument.argument ` to get the failed argument's value (:issue:`9283`). +- |commands| Add :attr:`Context.filesize_limit ` property (:issue:`9416`). +- |commands| Add support for :attr:`Parameter.displayed_name ` (:issue:`9427`). + +Bug Fixes +~~~~~~~~~~ + +- Fix ``FileHandler`` handlers being written ANSI characters when the bot is executed inside PyCharm. + - 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 `_. + +- Fix channel edits with :meth:`Webhook.edit` sending two requests instead of one. +- Fix :attr:`StageChannel.last_message_id` always being ``None`` (:issue:`9422`). +- Fix piped audio input ending prematurely (:issue:`9001`, :issue:`9380`). +- Fix persistent detection for :class:`ui.TextInput` being incorrect if the ``custom_id`` is set later (:issue:`9438`). +- Fix custom attributes not being copied over when inheriting from :class:`app_commands.Group` (:issue:`9383`). +- Fix AutoMod audit log entry error due to empty channel_id (:issue:`9384`). +- Fix handling of ``around`` parameter in :meth:`abc.Messageable.history` (:issue:`9388`). +- Fix occasional :exc:`AttributeError` when accessing the :attr:`ClientUser.mutual_guilds` property (:issue:`9387`). +- Fix :func:`utils.escape_markdown` not escaping the new markdown (:issue:`9361`). +- Fix webhook targets not being converted in audit logs (:issue:`9332`). +- Fix error when not passing ``enabled`` in :meth:`Guild.create_automod_rule` (:issue:`9292`). +- Fix how various parameters are handled in :meth:`Guild.create_scheduled_event` (:issue:`9275`). +- Fix not sending the ``ssrc`` parameter when sending the SPEAKING payload (:issue:`9301`). +- Fix :attr:`Message.guild` being ``None`` sometimes when received via an interaction. +- Fix :attr:`Message.system_content` for :attr:`MessageType.channel_icon_change` (:issue:`9410`). + +Miscellaneous +~~~~~~~~~~~~~~ + +- Update the base :attr:`Guild.filesize_limit` to 25MiB (:issue:`9353`). +- Allow Interaction webhook URLs to be used in :meth:`Webhook.from_url`. +- Set the socket family of internal connector to ``AF_INET`` to prevent IPv6 connections (:issue:`9442`, :issue:`9443`). + .. _vp2p2p3: v2.2.3 From c785e4ba755e9b25c36953bdb0ffc34b41fe24d7 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 12 Jun 2023 13:40:41 -0400 Subject: [PATCH 50/64] Version bump to v2.3 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 39fdb2673..6461cfe71 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.3.0a' +__version__ = '2.3.0' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -78,7 +78,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=0, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 0c6a2fc3d5a1c30cb92c45331ff0b3e487e088f8 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 12 Jun 2023 14:03:02 -0400 Subject: [PATCH 51/64] 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 6461cfe71..a72b9969e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.3.0' +__version__ = '2.4.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -78,7 +78,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=0, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From dc4ed438a5ba780fcaaf3fb46bbc603827d87bf2 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 12 Jun 2023 19:39:41 -0400 Subject: [PATCH 52/64] Fix plain username lookup for Guild.get_member_named Fix #9451 --- discord/ext/commands/converter.py | 10 ++++++++++ discord/guild.py | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 4cd8a3533..30cea2a5c 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -207,6 +207,11 @@ class MemberConverter(IDConverter[discord.Member]): async def query_member_named(self, guild: discord.Guild, argument: str) -> Optional[discord.Member]: cache = guild._state.member_cache_flags.joined username, _, discriminator = argument.rpartition('#') + + # If # isn't found then "discriminator" actually has the username + if not username: + discriminator, username = username, discriminator + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): lookup = username predicate = lambda m: m.name == username and m.discriminator == discriminator @@ -316,6 +321,11 @@ class UserConverter(IDConverter[discord.User]): return result # type: ignore username, _, discriminator = argument.rpartition('#') + + # If # isn't found then "discriminator" actually has the username + if not username: + discriminator, username = username, discriminator + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): predicate = lambda u: u.name == username and u.discriminator == discriminator else: diff --git a/discord/guild.py b/discord/guild.py index a37fb549f..f7d1a5566 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1104,6 +1104,11 @@ class Guild(Hashable): members = self.members username, _, discriminator = name.rpartition('#') + + # If # isn't found then "discriminator" actually has the username + if not username: + discriminator, username = username, discriminator + if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): return utils.find(lambda m: m.name == username and m.discriminator == discriminator, members) From 49e31e9e23517471252cd3f992fa3c88607741ff Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 17 Jun 2023 01:24:10 -0400 Subject: [PATCH 53/64] Use cache data if available for Interaction.channel The data from Discord does not contain all the attributes that the cached data has. There may be a slight chance that the interaction data is more up to date than the cached data or vice versa but more information tends to trump over this slight chance. --- discord/interactions.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 44645396e..1cf029451 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -184,22 +184,6 @@ class Interaction(Generic[ClientT]): self.version: int = data['version'] self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') self.channel: Optional[InteractionChannel] = None - - raw_channel = data.get('channel', {}) - raw_ch_type = raw_channel.get('type') - if raw_ch_type is not None: - factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None - if factory is None: - logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel)) - else: - if ch_type in (ChannelType.group, ChannelType.private): - channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore - else: - guild = self._state._get_or_create_unavailable_guild(self.guild_id) # type: ignore - channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore - - self.channel = channel - self.application_id: int = int(data['application_id']) self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US')) @@ -220,6 +204,7 @@ class Interaction(Generic[ClientT]): self._permissions: int = 0 self._app_permissions: int = int(data.get('app_permissions', 0)) + guild = None if self.guild_id: guild = self._state._get_or_create_unavailable_guild(self.guild_id) @@ -240,6 +225,22 @@ class Interaction(Generic[ClientT]): except KeyError: pass + raw_channel = data.get('channel', {}) + channel_id = utils._get_as_snowflake(raw_channel, 'id') + if channel_id is not None and guild is not None: + self.channel = guild and guild._resolve_channel(channel_id) + + raw_ch_type = raw_channel.get('type') + if self.channel is None and raw_ch_type is not None: + factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None + if factory is None: + logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel)) + else: + if ch_type in (ChannelType.group, ChannelType.private): + self.channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore + elif guild is not None: + self.channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore + @property def client(self) -> ClientT: """:class:`Client`: The client that is handling this interaction. From a7f2c670c9ed1e9196d170239d6c2156a7aba503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 21 Jun 2023 14:30:25 +0100 Subject: [PATCH 54/64] Fix false positives in animated for PartialEmoji.from_str --- discord/partial_emoji.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index 17c528c52..7d366949c 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -94,7 +94,7 @@ class PartialEmoji(_EmojiTag, AssetMixin): __slots__ = ('animated', 'name', 'id', '_state') - _CUSTOM_EMOJI_RE = re.compile(r'a)?:?(?P[A-Za-z0-9\_]+):(?P[0-9]{13,20})>?') + _CUSTOM_EMOJI_RE = re.compile(r'a)?:)?(?P[A-Za-z0-9\_]+):(?P[0-9]{13,20})>?') if TYPE_CHECKING: id: Optional[int] From 0efc05ccce5a7c03d5fe976cbfc19dbbafc3126a Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Fri, 23 Jun 2023 06:19:52 +0200 Subject: [PATCH 55/64] Fix certain select types not appearing in Message.components --- discord/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/components.py b/discord/components.py index c0a213efa..5c3679b13 100644 --- a/discord/components.py +++ b/discord/components.py @@ -527,7 +527,7 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti return ActionRow(data) elif data['type'] == 2: return Button(data) - elif data['type'] == 3: - return SelectMenu(data) elif data['type'] == 4: return TextInput(data) + elif data['type'] in (3, 5, 6, 7, 8): + return SelectMenu(data) From 2fdbe59376d736483cd1226e674e609433877af4 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Fri, 23 Jun 2023 00:19:17 -0400 Subject: [PATCH 56/64] Fix Message.channel being None for interactions --- discord/interactions.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/discord/interactions.py b/discord/interactions.py index 1cf029451..595414b2f 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -193,6 +193,26 @@ class Interaction(Generic[ClientT]): except KeyError: self.guild_locale = None + guild = None + if self.guild_id: + guild = self._state._get_or_create_unavailable_guild(self.guild_id) + + raw_channel = data.get('channel', {}) + channel_id = utils._get_as_snowflake(raw_channel, 'id') + if channel_id is not None and guild is not None: + self.channel = guild and guild._resolve_channel(channel_id) + + raw_ch_type = raw_channel.get('type') + if self.channel is None and raw_ch_type is not None: + factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None + if factory is None: + logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel)) + else: + if ch_type in (ChannelType.group, ChannelType.private): + self.channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore + elif guild is not None: + self.channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore + self.message: Optional[Message] try: # The channel and message payloads are mismatched yet handled properly at runtime @@ -204,10 +224,7 @@ class Interaction(Generic[ClientT]): self._permissions: int = 0 self._app_permissions: int = int(data.get('app_permissions', 0)) - guild = None - if self.guild_id: - guild = self._state._get_or_create_unavailable_guild(self.guild_id) - + if guild is not None: # Upgrade Message.guild in case it's missing with partial guild data if self.message is not None and self.message.guild is None: self.message.guild = guild @@ -225,22 +242,6 @@ class Interaction(Generic[ClientT]): except KeyError: pass - raw_channel = data.get('channel', {}) - channel_id = utils._get_as_snowflake(raw_channel, 'id') - if channel_id is not None and guild is not None: - self.channel = guild and guild._resolve_channel(channel_id) - - raw_ch_type = raw_channel.get('type') - if self.channel is None and raw_ch_type is not None: - factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None - if factory is None: - logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel)) - else: - if ch_type in (ChannelType.group, ChannelType.private): - self.channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore - elif guild is not None: - self.channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore - @property def client(self) -> ClientT: """:class:`Client`: The client that is handling this interaction. From 5e86be3b7260426e26cf38a067cbda94a2b68652 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 03:58:34 -0400 Subject: [PATCH 57/64] [commands] Change lookup order to place nicknames last --- discord/ext/commands/converter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 30cea2a5c..7255f1715 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -188,9 +188,9 @@ class MemberConverter(IDConverter[discord.Member]): 2. Lookup by mention. 3. Lookup by username#discriminator (deprecated). 4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator). - 5. Lookup by guild nickname. + 5. Lookup by user name. 6. Lookup by global name. - 7. Lookup by user name. + 7. Lookup by guild nickname. .. versionchanged:: 1.5 Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` @@ -217,7 +217,7 @@ class MemberConverter(IDConverter[discord.Member]): predicate = lambda m: m.name == username and m.discriminator == discriminator else: lookup = argument - predicate = lambda m: m.nick == argument or m.global_name == argument or m.name == argument + predicate = lambda m: m.name == argument or m.global_name == argument or m.nick == argument members = await guild.query_members(lookup, limit=100, cache=cache) return discord.utils.find(predicate, members) @@ -289,8 +289,8 @@ class UserConverter(IDConverter[discord.User]): 2. Lookup by mention. 3. Lookup by username#discriminator (deprecated). 4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator). - 5. Lookup by global name. - 6. Lookup by user name. + 5. Lookup by user name. + 6. Lookup by global name. .. versionchanged:: 1.5 Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` @@ -329,7 +329,7 @@ class UserConverter(IDConverter[discord.User]): if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()): predicate = lambda u: u.name == username and u.discriminator == discriminator else: - predicate = lambda u: u.global_name == argument or u.name == argument + predicate = lambda u: u.name == argument or u.global_name == argument result = discord.utils.find(predicate, state._users.values()) if result is None: From d48116af3b9e981b0f074d81f95e9319346074b4 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 04:13:18 -0400 Subject: [PATCH 58/64] Add changelog for v2.3.1 --- docs/whats_new.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 04effd7de..2de164435 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,22 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p3p1: + +v2.3.1 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix username lookup in :meth:`Guild.get_member_named` (:issue:`9451`). +- Use cache data first for :attr:`Interaction.channel` instead of API data. + - This bug usually manifested in incomplete channel objects (e.g. no ``overwrites``) because Discord does not provide this data. + +- Fix false positives in :meth:`PartialEmoji.from_str` inappropriately setting ``animated`` to ``True`` (:issue:`9456`, :issue:`9457`). +- Fix certain select types not appearing in :attr:`Message.components` (:issue:`9462`). +- |commands| Change lookup order for :class:`~ext.commands.MemberConverter` and :class:`~ext.commands.UserConverter` to prioritise usernames instead of nicknames. + .. _vp2p3p0: v2.3.0 From 2a9640b5160194d81ee32584d772a445141b00ab Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 04:13:50 -0400 Subject: [PATCH 59/64] Version bump to v2.3.1 --- discord/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index a72b9969e..85183ccb8 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.4.0a' +__version__ = '2.3.1' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -78,7 +78,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=3, micro=1, releaselevel='final', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From c6bdb0b0dd908f4fc4986143dad13bb240af2ce5 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 04:14:15 -0400 Subject: [PATCH 60/64] 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 85183ccb8..a72b9969e 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -13,7 +13,7 @@ __title__ = 'discord' __author__ = 'Rapptz' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' -__version__ = '2.3.1' +__version__ = '2.4.0a' __path__ = __import__('pkgutil').extend_path(__path__, __name__) @@ -78,7 +78,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=1, releaselevel='final', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) From 03140c0163261e0bd60ffb9121d13d19c619152d Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 04:31:36 -0400 Subject: [PATCH 61/64] [tasks] Add name parameter to give the internal task a name --- discord/ext/tasks/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index cf175b7a8..0a0f38413 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -144,6 +144,7 @@ class Loop(Generic[LF]): time: Union[datetime.time, Sequence[datetime.time]], count: Optional[int], reconnect: bool, + name: Optional[str], ) -> None: self.coro: LF = coro self.reconnect: bool = reconnect @@ -165,6 +166,7 @@ class Loop(Generic[LF]): self._is_being_cancelled = False self._has_failed = False self._stop_next_iteration = False + self._name: str = f'discord-ext-tasks: {coro.__qualname__}' if name is None else name if self.count is not None and self.count <= 0: raise ValueError('count must be greater than 0 or None.') @@ -395,7 +397,7 @@ class Loop(Generic[LF]): args = (self._injected, *args) self._has_failed = False - self._task = asyncio.create_task(self._loop(*args, **kwargs)) + self._task = asyncio.create_task(self._loop(*args, **kwargs), name=self._name) return self._task def stop(self) -> None: @@ -770,6 +772,7 @@ def loop( time: Union[datetime.time, Sequence[datetime.time]] = MISSING, count: Optional[int] = None, reconnect: bool = True, + name: Optional[str] = None, ) -> Callable[[LF], Loop[LF]]: """A decorator that schedules a task in the background for you with optional reconnect logic. The decorator returns a :class:`Loop`. @@ -802,6 +805,12 @@ def loop( Whether to handle errors and restart the task using an exponential back-off algorithm similar to the one used in :meth:`discord.Client.connect`. + name: Optional[:class:`str`] + The name to assign to the internal task. By default + it is assigned a name based off of the callable name + such as ``discord-ext-tasks: function_name``. + + .. versionadded:: 2.4 Raises -------- @@ -821,6 +830,7 @@ def loop( count=count, time=time, reconnect=reconnect, + name=name, ) return decorator From 646ab85bb62244666f092adb8683d2890f953664 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Mon, 26 Jun 2023 05:39:36 -0400 Subject: [PATCH 62/64] [tasks] Fix missing name parameter in loop copy --- discord/ext/tasks/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 0a0f38413..1ffb0b851 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -284,6 +284,7 @@ class Loop(Generic[LF]): time=self._time, count=self.count, reconnect=self.reconnect, + name=self._name, ) copy._injected = obj copy._before_loop = self._before_loop From 33243f9bd6395146c297c03040bd843b696d709c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aluerie=20=E2=9D=A4=236524?= <33165440+Aluerie@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:01:56 -0700 Subject: [PATCH 63/64] Fix Intents.emoji and emojis_and_stickers swapped alias decorators --- discord/flags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/flags.py b/discord/flags.py index 0fb60084d..27eaf2628 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -850,7 +850,7 @@ class Intents(BaseFlags): """ return 1 << 2 - @flag_value + @alias_flag_value def emojis(self): """:class:`bool`: Alias of :attr:`.emojis_and_stickers`. @@ -859,7 +859,7 @@ class Intents(BaseFlags): """ return 1 << 3 - @alias_flag_value + @flag_value def emojis_and_stickers(self): """:class:`bool`: Whether guild emoji and sticker related events are enabled. From 0871b34fc86af49deb6db2436d3e77f5516fdcba Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Jun 2023 08:20:34 -0400 Subject: [PATCH 64/64] [commands] Revert on_error when cog is ejected for HelpCommand --- discord/ext/commands/help.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 32228d205..163bc2694 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -294,6 +294,9 @@ class _HelpCommandImpl(Command): cog.walk_commands = cog.walk_commands.__wrapped__ self.cog = None + # Revert `on_error` to use the original one in case of race conditions + self.on_error = self._injected.on_help_command_error + class HelpCommand: r"""The base implementation for help command formatting.