From a183a56dd98dd14f49b9229578664a91ed0daa9e Mon Sep 17 00:00:00 2001 From: Pipythonmc <47196755+pythonmcpi@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:11:12 -0700 Subject: [PATCH 01/29] Fix _get_command_error improperly handling some error messages --- discord/app_commands/errors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index dc63f10e8..87a5dbb59 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -485,6 +485,10 @@ def _get_command_error( if key == 'options': for index, d in remaining.items(): _get_command_error(index, d, children, messages, indent=indent + 2) + elif key == '_errors': + errors = [x.get('message', '') for x in remaining] + + messages.extend(f'{indentation} {message}' for message in errors) else: if isinstance(remaining, dict): try: @@ -493,8 +497,6 @@ def _get_command_error( errors = _flatten_error_dict(remaining, key=key) else: errors = {key: ' '.join(x.get('message', '') for x in inner_errors)} - else: - errors = _flatten_error_dict(remaining, key=key) messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) From c41cadfa91a2e490c3d42cdf19412cbcef190f3c Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 15:22:35 -0400 Subject: [PATCH 02/29] Fix introduced potential TypeError with _get_command_error --- discord/app_commands/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index 87a5dbb59..abdc9f2f0 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -498,7 +498,8 @@ def _get_command_error( else: errors = {key: ' '.join(x.get('message', '') for x in inner_errors)} - messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) + if isinstance(errors, dict): + messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) class CommandSyncFailure(AppCommandError, HTTPException): From da89fbc8b5b6f0cb994d8575c04d4219b38c13a6 Mon Sep 17 00:00:00 2001 From: Michael H Date: Wed, 28 Aug 2024 15:23:51 -0400 Subject: [PATCH 03/29] Re-add client connector param This provides paths for users to handle two entirely seperate issues - Alternative fix for #9870 - Allows handling of windows sslcontext issues without a global truststore.inject_into_ssl() use --- discord/client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/discord/client.py b/discord/client.py index a91be7160..4f5dfe9f0 100644 --- a/discord/client.py +++ b/discord/client.py @@ -249,6 +249,11 @@ class Client: set to is ``30.0`` seconds. .. versionadded:: 2.0 + connector: Optional[:class:`aiohttp.BaseConnector`] + The aiohhtp connector to use for this client. This can be used to control underlying aiohttp + behavior, such as setting a dns resolver or sslcontext. + + .. versionadded:: 2.5 Attributes ----------- @@ -264,6 +269,7 @@ class Client: self.shard_id: Optional[int] = options.get('shard_id') self.shard_count: Optional[int] = options.get('shard_count') + connector: Optional[aiohttp.BaseConnector] = options.get('connector', None) proxy: Optional[str] = options.pop('proxy', None) proxy_auth: Optional[aiohttp.BasicAuth] = options.pop('proxy_auth', None) unsync_clock: bool = options.pop('assume_unsync_clock', True) @@ -271,6 +277,7 @@ class Client: max_ratelimit_timeout: Optional[float] = options.pop('max_ratelimit_timeout', None) self.http: HTTPClient = HTTPClient( self.loop, + connector, proxy=proxy, proxy_auth=proxy_auth, unsync_clock=unsync_clock, From 38bbfed7174bccf5490e5f931b3d8ba8e15922b9 Mon Sep 17 00:00:00 2001 From: ambdroid <61042504+ambdroid@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:31:38 -0400 Subject: [PATCH 04/29] Fix Poll.duration rounding error --- discord/poll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/poll.py b/discord/poll.py index 7dc3897ac..88ed5b534 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -384,9 +384,9 @@ class Poll: question_data = data.get('question') question = question_data.get('text') expiry = utils.parse_time(data['expiry']) # If obtained via API, then expiry is set. - duration = expiry - message.created_at + # expiry - message.created_at may be a few nanos away from the actual duration + duration = datetime.timedelta(hours=round((expiry - message.created_at).total_seconds() / 3600)) # self.created_at = message.created_at - # duration = self.created_at - expiry self = cls( duration=duration, From a6d1dc04555ef0395c26e11c5822164c49f805fd Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:32:20 +0200 Subject: [PATCH 05/29] Add support for getting the attachment's title --- discord/message.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/message.py b/discord/message.py index 1d1a3c96c..0247ee8c1 100644 --- a/discord/message.py +++ b/discord/message.py @@ -194,6 +194,10 @@ class Attachment(Hashable): The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message. .. versionadded:: 2.3 + title: Optional[:class:`str`] + The normalised version of the attachment's filename. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -211,6 +215,7 @@ class Attachment(Hashable): 'duration', 'waveform', '_flags', + 'title', ) def __init__(self, *, data: AttachmentPayload, state: ConnectionState): @@ -226,6 +231,7 @@ class Attachment(Hashable): self.description: Optional[str] = data.get('description') self.ephemeral: bool = data.get('ephemeral', False) self.duration: Optional[float] = data.get('duration_secs') + self.title: Optional[str] = data.get('title') waveform = data.get('waveform') self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None From 0a2faa6f5dce2a06973d22c7193040d8127cfae7 Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:04:33 +0530 Subject: [PATCH 06/29] Fix default_avatar for team user and webhook --- discord/user.py | 2 +- discord/webhook/async_.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/discord/user.py b/discord/user.py index 5151957dc..c5391372a 100644 --- a/discord/user.py +++ b/discord/user.py @@ -171,7 +171,7 @@ class BaseUser(_UserTag): @property def default_avatar(self) -> Asset: """:class:`Asset`: Returns the default avatar for a given user.""" - if self.discriminator == '0': + if self.discriminator in ('0', '0000'): avatar_id = (self.id >> 22) % len(DefaultAvatar) else: avatar_id = int(self.discriminator) % 5 diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 3b0416f9a..91b021a5e 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -38,7 +38,7 @@ import aiohttp from .. import utils from ..errors import HTTPException, Forbidden, NotFound, DiscordServerError from ..message import Message -from ..enums import try_enum, WebhookType, ChannelType +from ..enums import try_enum, WebhookType, ChannelType, DefaultAvatar from ..user import BaseUser, User from ..flags import MessageFlags from ..asset import Asset @@ -360,7 +360,7 @@ class AsyncWebhookAdapter: multipart: Optional[List[Dict[str, Any]]] = None, files: Optional[Sequence[File]] = None, thread_id: Optional[int] = None, - ) -> Response[Message]: + ) -> Response[MessagePayload]: route = Route( 'PATCH', '/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}', @@ -1049,12 +1049,11 @@ class BaseWebhook(Hashable): @property def default_avatar(self) -> Asset: """ - :class:`Asset`: Returns the default avatar. This is always the blurple avatar. + :class:`Asset`: Returns the default avatar. .. versionadded:: 2.0 """ - # Default is always blurple apparently - return Asset._from_default_avatar(self._state, 0) + return Asset._from_default_avatar(self._state, (self.id >> 22) % len(DefaultAvatar)) @property def display_avatar(self) -> Asset: From 1c6f3c5ff1f8ac0fd0f49280a0309d656c918b33 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:37:32 +0200 Subject: [PATCH 07/29] [docs] Remove pack_id attribute from Sticker [docs] Remove unnecessary pack_id --- discord/sticker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/sticker.py b/discord/sticker.py index 30eb62c70..96deef475 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -258,8 +258,6 @@ class Sticker(_StickerTag): The id of the sticker. description: :class:`str` The description of the sticker. - pack_id: :class:`int` - The id of the sticker's pack. format: :class:`StickerFormatType` The format for the sticker's image. url: :class:`str` From 7cf6df166d522867ee73426662db99eb370cf2c1 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Thu, 29 Aug 2024 01:18:19 +0530 Subject: [PATCH 08/29] Fix url for GIF StickerItem --- discord/sticker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/sticker.py b/discord/sticker.py index 96deef475..ebaf1534c 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -203,7 +203,10 @@ class StickerItem(_StickerTag): self.name: str = data['name'] self.id: int = int(data['id']) self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type']) - self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' + if self.format is StickerFormatType.gif: + self.url: str = f'https://media.discordapp.net/stickers/{self.id}.gif' + else: + self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}' def __repr__(self) -> str: return f'' From 794f2bf149d8849a28acadefe0dac871417a6bfc Mon Sep 17 00:00:00 2001 From: fretgfr <51489753+fretgfr@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:48:38 -0400 Subject: [PATCH 09/29] [docs] correct hyperlink to discord docs --- discord/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/embeds.py b/discord/embeds.py index 6a79fef71..2071ef9db 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -199,7 +199,7 @@ class Embed: """Converts a :class:`dict` to a :class:`Embed` provided it is in the format that Discord expects it to be in. - You can find out about this format in the :ddocs:`official Discord documentation `. + You can find out about this format in the :ddocs:`official Discord documentation `. Parameters ----------- From 8104ff2ad472ced922bd2dd14d34ab01f40aa014 Mon Sep 17 00:00:00 2001 From: lmaotrigine <57328245+lmaotrigine@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:20:51 +0530 Subject: [PATCH 10/29] [docs] Fix typehint for Embed.set_(image,thumbnail) --- discord/embeds.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index 2071ef9db..8b2cecb91 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -413,8 +413,9 @@ class Embed: Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the image. Only HTTP(S) is supported. + If ``None`` is passed, any existing image is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ @@ -457,8 +458,9 @@ class Embed: Parameters ----------- - url: :class:`str` + url: Optional[:class:`str`] The source URL for the thumbnail. Only HTTP(S) is supported. + If ``None`` is passed, any existing thumbnail is removed. Inline attachment URLs are also supported, see :ref:`local_image`. """ From 3018fee4432c5718fe90aff61cfe4b7932b95f60 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 15:51:17 -0400 Subject: [PATCH 11/29] Remove stale documentation in Embed.set_thumbnail --- discord/embeds.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index 8b2cecb91..258ef0dfd 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -453,9 +453,6 @@ class Embed: This function returns the class instance to allow for fluent-style chaining. - .. versionchanged:: 1.4 - Passing ``None`` removes the thumbnail. - Parameters ----------- url: Optional[:class:`str`] From 9ab938a9ea5ffcd13c8a0d8fbc662ef1a651ca5a Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:54:34 +0200 Subject: [PATCH 12/29] Add Guild.fetch_role --- discord/guild.py | 31 +++++++++++++++++++++++++++++++ discord/http.py | 3 +++ 2 files changed, 34 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index f34818b63..5296faa07 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3408,6 +3408,37 @@ class Guild(Hashable): data = await self._state.http.get_roles(self.id) return [Role(guild=self, state=self._state, data=d) for d in data] + async def fetch_role(self, role_id: int, /) -> Role: + """|coro| + + Retrieves a :class:`Role` with the specified ID. + + .. versionadded:: 2.5 + + .. note:: + + This method is an API call. For general usage, consider :attr:`get_role` instead. + + Parameters + ---------- + role_id: :class:`int` + The role's ID. + + Raises + ------- + NotFound + The role requested could not be found. + HTTPException + An error occurred fetching the role. + + Returns + ------- + :class:`Role` + The retrieved role. + """ + data = await self._state.http.get_role(self.id, role_id) + return Role(guild=self, state=self._state, data=data) + @overload async def create_role( self, diff --git a/discord/http.py b/discord/http.py index 608595fe3..08daa6efc 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1858,6 +1858,9 @@ class HTTPClient: def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]: return self.request(Route('GET', '/guilds/{guild_id}/roles', guild_id=guild_id)) + def get_role(self, guild_id: Snowflake, role_id: Snowflake) -> Response[role.Role]: + return self.request(Route('GET', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id)) + def edit_role( self, guild_id: Snowflake, role_id: Snowflake, *, reason: Optional[str] = None, **fields: Any ) -> Response[role.Role]: From d578709640356fd6195df4a8cfd80d9638a6ec52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 28 Aug 2024 21:00:27 +0100 Subject: [PATCH 13/29] Add approximate_user_install_count to AppInfo --- discord/appinfo.py | 6 ++++++ discord/types/appinfo.py | 1 + 2 files changed, 7 insertions(+) diff --git a/discord/appinfo.py b/discord/appinfo.py index 074892d05..932f852c2 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -147,6 +147,10 @@ class AppInfo: The approximate count of the guilds the bot was added to. .. versionadded:: 2.4 + approximate_user_install_count: Optional[:class:`int`] + The approximate count of the user-level installations the bot has. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -175,6 +179,7 @@ class AppInfo: 'interactions_endpoint_url', 'redirect_uris', 'approximate_guild_count', + 'approximate_user_install_count', ) def __init__(self, state: ConnectionState, data: AppInfoPayload): @@ -212,6 +217,7 @@ class AppInfo: self.interactions_endpoint_url: Optional[str] = data.get('interactions_endpoint_url') self.redirect_uris: List[str] = data.get('redirect_uris', []) self.approximate_guild_count: int = data.get('approximate_guild_count', 0) + self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count') def __repr__(self) -> str: return ( diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index e291babfa..ae7fc7e0d 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -45,6 +45,7 @@ class BaseAppInfo(TypedDict): summary: str description: str flags: int + approximate_user_install_count: NotRequired[int] cover_image: NotRequired[str] terms_of_service_url: NotRequired[str] privacy_policy_url: NotRequired[str] From f9dbe60fc41771c3914fef1a6f86f7c14ca61ddc Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 28 Aug 2024 16:05:39 -0400 Subject: [PATCH 14/29] Revert "Set socket family of connector to AF_INET" This change was made since Discord doesn't support IPv6, and there were concerns about clients with DNS64 enabled without NAT64. However, this breaks hosts who don't have v4 connectivity and are _actually_ running NAT64. Having DNS64 without NAT64 is really an issue on the client's end. It would break far more than just discord.py, so I don't think we should be concerned about those cases. --- discord/http.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index 08daa6efc..a16c03629 100644 --- a/discord/http.py +++ b/discord/http.py @@ -48,7 +48,6 @@ from typing import ( from urllib.parse import quote as _uriquote from collections import deque import datetime -import socket import aiohttp @@ -798,8 +797,7 @@ 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: - # discord does not support ipv6 - self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET) + self.connector = aiohttp.TCPConnector(limit=0) self.__session = aiohttp.ClientSession( connector=self.connector, From 34bf026a0213902f821173858cf80d2ab067e51a Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:15:26 +0200 Subject: [PATCH 15/29] Add support for get sticker pack --- discord/client.py | 27 +++++++++++++++++++++++++++ discord/http.py | 3 +++ discord/sticker.py | 18 +++++++----------- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/discord/client.py b/discord/client.py index 4f5dfe9f0..601dbbaf4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2926,6 +2926,33 @@ class Client: data = await self.http.list_premium_sticker_packs() return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']] + async def fetch_premium_sticker_pack(self, sticker_pack_id: int, /) -> StickerPack: + """|coro| + + Retrieves a premium sticker pack with the specified ID. + + .. versionadded:: 2.5 + + Parameters + ---------- + sticker_pack_id: :class:`int` + The sticker pack's ID to fetch from. + + Raises + ------- + NotFound + A sticker pack with this ID does not exist. + HTTPException + Retrieving the sticker pack failed. + + Returns + ------- + :class:`.StickerPack` + The retrieved premium sticker pack. + """ + data = await self.http.get_sticker_pack(sticker_pack_id) + return StickerPack(state=self._connection, data=data) + async def create_dm(self, user: Snowflake) -> DMChannel: """|coro| diff --git a/discord/http.py b/discord/http.py index a16c03629..d7e33b560 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1609,6 +1609,9 @@ class HTTPClient: def get_sticker(self, sticker_id: Snowflake) -> Response[sticker.Sticker]: return self.request(Route('GET', '/stickers/{sticker_id}', sticker_id=sticker_id)) + def get_sticker_pack(self, sticker_pack_id: Snowflake) -> Response[sticker.StickerPack]: + return self.request(Route('GET', '/sticker-packs/{sticker_pack_id}', sticker_pack_id=sticker_pack_id)) + def list_premium_sticker_packs(self) -> Response[sticker.ListPremiumStickerPacks]: return self.request(Route('GET', '/sticker-packs')) diff --git a/discord/sticker.py b/discord/sticker.py index ebaf1534c..bf90f8866 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -28,8 +28,7 @@ import unicodedata from .mixins import Hashable from .asset import Asset, AssetMixin -from .utils import cached_slot_property, find, snowflake_time, get, MISSING, _get_as_snowflake -from .errors import InvalidData +from .utils import cached_slot_property, snowflake_time, get, MISSING, _get_as_snowflake from .enums import StickerType, StickerFormatType, try_enum __all__ = ( @@ -51,7 +50,6 @@ if TYPE_CHECKING: Sticker as StickerPayload, StandardSticker as StandardStickerPayload, GuildSticker as GuildStickerPayload, - ListPremiumStickerPacks as ListPremiumStickerPacksPayload, ) @@ -353,9 +351,12 @@ class StandardSticker(Sticker): Retrieves the sticker pack that this sticker belongs to. + .. versionchanged:: 2.5 + Now raises ``NotFound`` instead of ``InvalidData``. + Raises -------- - InvalidData + NotFound The corresponding sticker pack was not found. HTTPException Retrieving the sticker pack failed. @@ -365,13 +366,8 @@ class StandardSticker(Sticker): :class:`StickerPack` The retrieved sticker pack. """ - data: ListPremiumStickerPacksPayload = await self._state.http.list_premium_sticker_packs() - packs = data['sticker_packs'] - pack = find(lambda d: int(d['id']) == self.pack_id, packs) - - if pack: - return StickerPack(state=self._state, data=pack) - raise InvalidData(f'Could not find corresponding sticker pack for {self!r}') + data = await self._state.http.get_sticker_pack(self.pack_id) + return StickerPack(state=self._state, data=data) class GuildSticker(Sticker): From aeab0d48fd7e6483a977995450515e69e2b30cdb Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 28 Aug 2024 22:15:58 +0200 Subject: [PATCH 16/29] Fix stacklevel for Message.interaction deprecation warning --- discord/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/utils.py b/discord/utils.py index 99c7cfc94..89cc8bdeb 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -302,7 +302,7 @@ def deprecated(instead: Optional[str] = None) -> Callable[[Callable[P, T]], Call else: fmt = '{0.__name__} is deprecated.' - warnings.warn(fmt.format(func, instead), stacklevel=3, category=DeprecationWarning) + warnings.warn(fmt.format(func, instead), stacklevel=2, category=DeprecationWarning) warnings.simplefilter('default', DeprecationWarning) # reset filter return func(*args, **kwargs) From 624b5b7643ce6dfc783aa9ce365026af41f78e4b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 28 Aug 2024 16:17:03 -0400 Subject: [PATCH 17/29] Use fallback audioop package for Python v3.13 or higher Fix #9477 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 046084ebb..ef2b6c534 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ aiohttp>=3.7.4,<4 +audioop-lts; python_version>='3.13' From 643a7f4e1d351cbe4928ed4915474a1295ebb480 Mon Sep 17 00:00:00 2001 From: Deep Jain Date: Wed, 28 Aug 2024 16:21:41 -0400 Subject: [PATCH 18/29] Add DummyCookieJar to client owned ClientSession --- discord/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/http.py b/discord/http.py index d7e33b560..494ecef0c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -803,6 +803,7 @@ class HTTPClient: connector=self.connector, ws_response_class=DiscordClientWebSocketResponse, trace_configs=None if self.http_trace is None else [self.http_trace], + cookie_jar=aiohttp.DummyCookieJar(), ) self._global_over = asyncio.Event() self._global_over.set() From fde7131d26bfa431a476a8a64ea3ae9f18bf2683 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:30:18 +0200 Subject: [PATCH 19/29] Add missing guild incident fields Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/guild.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 5296faa07..07a072d1f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4435,6 +4435,28 @@ class Guild(Hashable): return utils.parse_time(self._incidents_data.get('dms_disabled_until')) + @property + def dm_spam_detected_at(self) -> Optional[datetime.datetime]: + """:class:`datetime.datetime`: Returns the time when DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('dm_spam_detected_at')) + + @property + def raid_detected_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self._incidents_data: + return None + + return utils.parse_time(self._incidents_data.get('raid_detected_at')) + def invites_paused(self) -> bool: """:class:`bool`: Whether invites are paused in the guild. @@ -4451,6 +4473,26 @@ class Guild(Hashable): .. versionadded:: 2.4 """ if not self.dms_paused_until: - return False + return 'INVITES_DISABLED' in self.features return self.dms_paused_until > utils.utcnow() + + def is_dm_spam_detected(self) -> bool: + """:class:`bool`: Whether DM spam was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.dm_spam_detected_at: + return False + + return self.dm_spam_detected_at > utils.utcnow() + + def is_raid_detected(self) -> bool: + """:class:`bool`: Whether a raid was detected in the guild. + + .. versionadded:: 2.5 + """ + if not self.raid_detected_at: + return False + + return self.raid_detected_at > utils.utcnow() From 62e52803a74de3471594c7ac294af3926f51edec Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:36:22 +0200 Subject: [PATCH 20/29] Add support Member.fetch_voice --- discord/http.py | 7 +++++++ discord/member.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/discord/http.py b/discord/http.py index 494ecef0c..e1bb04483 100644 --- a/discord/http.py +++ b/discord/http.py @@ -92,6 +92,7 @@ if TYPE_CHECKING: welcome_screen, sku, poll, + voice, ) from .types.snowflake import Snowflake, SnowflakeList @@ -1147,6 +1148,12 @@ class HTTPClient: r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) return self.request(r, json=fields, reason=reason) + def get_my_voice_state(self, guild_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)) + + def get_voice_state(self, guild_id: Snowflake, user_id: Snowflake) -> Response[voice.GuildVoiceState]: + return self.request(Route('GET', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)) + # Channel management def edit_channel( diff --git a/discord/member.py b/discord/member.py index 2eadacd27..66c771572 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1153,6 +1153,40 @@ class Member(discord.abc.Messageable, _UserTag): for role in roles: await req(guild_id, user_id, role.id, reason=reason) + async def fetch_voice(self) -> VoiceState: + """|coro| + + Retrieves the current voice state from this member. + + .. versionadded:: 2.5 + + Raises + ------- + NotFound + The member is not in a voice channel. + Forbidden + You do not have permissions to get a voice state. + HTTPException + Retrieving the voice state failed. + + Returns + ------- + :class:`VoiceState` + The current voice state of the member. + """ + guild_id = self.guild.id + if self._state.self_id == self.id: + data = await self._state.http.get_my_voice_state(guild_id) + else: + data = await self._state.http.get_voice_state(guild_id, self.id) + + channel_id = data.get('channel_id') + channel: Optional[VocalGuildChannel] = None + if channel_id is not None: + channel = self.guild.get_channel(int(channel_id)) # type: ignore # must be voice channel here + + return VoiceState(data=data, channel=channel) + def get_role(self, role_id: int, /) -> Optional[Role]: """Returns a role with the given ID from roles which the member has. From 463b4bd570faba36f1d8eac1ae29413bce3da9a6 Mon Sep 17 00:00:00 2001 From: Soheab <33902984+Soheab@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:15:15 +0200 Subject: [PATCH 21/29] Add support for application emojis Co-authored-by: DA344 <108473820+DA-344@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/client.py | 102 ++++++++++++++++++++++++++++++++++++++- discord/emoji.py | 49 ++++++++++++++++++- discord/http.py | 57 +++++++++++++++++++++- discord/types/appinfo.py | 5 ++ 4 files changed, 209 insertions(+), 4 deletions(-) diff --git a/discord/client.py b/discord/client.py index 601dbbaf4..1d2c2a326 100644 --- a/discord/client.py +++ b/discord/client.py @@ -366,7 +366,13 @@ class Client: @property def emojis(self) -> Sequence[Emoji]: - """Sequence[:class:`.Emoji`]: The emojis that the connected client has.""" + """Sequence[:class:`.Emoji`]: The emojis that the connected client has. + + .. note:: + + This not include the emojis that are owned by the application. + Use :meth:`.fetch_application_emoji` to get those. + """ return self._connection.emojis @property @@ -3073,3 +3079,97 @@ class Client: .. versionadded:: 2.0 """ return self._connection.persistent_views + + async def create_application_emoji( + self, + *, + name: str, + image: bytes, + ) -> Emoji: + """|coro| + + Create an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The emoji name. Must be at least 2 characters. + image: :class:`bytes` + The :term:`py:bytes-like object` representing the image data to use. + Only JPG, PNG and GIF images are supported. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Creating the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji that was created. + """ + if self.application_id is None: + raise MissingApplicationID + + img = utils._bytes_to_base64_data(image) + data = await self.http.create_application_emoji(self.application_id, name, img) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emoji(self, emoji_id: int, /) -> Emoji: + """|coro| + + Retrieves an emoji for the current application. + + .. versionadded:: 2.5 + + Parameters + ---------- + emoji_id: :class:`int` + The emoji ID to retrieve. + + Raises + ------ + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emoji failed. + + Returns + ------- + :class:`.Emoji` + The emoji requested. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emoji(self.application_id, emoji_id) + return Emoji(guild=Object(0), state=self._connection, data=data) + + async def fetch_application_emojis(self) -> List[Emoji]: + """|coro| + + Retrieves all emojis for the current application. + + .. versionadded:: 2.5 + + Raises + ------- + MissingApplicationID + The application ID could not be found. + HTTPException + Retrieving the emojis failed. + + Returns + ------- + List[:class:`.Emoji`] + The list of emojis for the current application. + """ + if self.application_id is None: + raise MissingApplicationID + + data = await self.http.get_application_emojis(self.application_id) + return [Emoji(guild=Object(0), state=self._connection, data=emoji) for emoji in data['items']] diff --git a/discord/emoji.py b/discord/emoji.py index 045486d5a..e011495fd 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -29,6 +29,8 @@ from .asset import Asset, AssetMixin from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag, PartialEmoji from .user import User +from .app_commands.errors import MissingApplicationID +from .object import Object # fmt: off __all__ = ( @@ -93,6 +95,10 @@ class Emoji(_EmojiTag, AssetMixin): user: Optional[:class:`User`] The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and having :attr:`~Permissions.manage_emojis`. + + Or if :meth:`.is_application_owned` is ``True``, this is the team member that uploaded + the emoji, or the bot user if it was uploaded using the API and this can + only be retrieved using :meth:`~discord.Client.fetch_application_emoji` or :meth:`~discord.Client.fetch_application_emojis`. """ __slots__: Tuple[str, ...] = ( @@ -108,7 +114,7 @@ class Emoji(_EmojiTag, AssetMixin): 'available', ) - def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload) -> None: + def __init__(self, *, guild: Snowflake, state: ConnectionState, data: EmojiPayload) -> None: self.guild_id: int = guild.id self._state: ConnectionState = state self._from_data(data) @@ -196,20 +202,32 @@ class Emoji(_EmojiTag, AssetMixin): Deletes the custom emoji. - You must have :attr:`~Permissions.manage_emojis` to do this. + You must have :attr:`~Permissions.manage_emojis` to do this if + :meth:`.is_application_owned` is ``False``. Parameters ----------- reason: Optional[:class:`str`] The reason for deleting this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to delete emojis. HTTPException An error occurred deleting the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing. """ + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + await self._state.http.delete_application_emoji(application_id, self.id) + return await self._state.http.delete_custom_emoji(self.guild_id, self.id, reason=reason) @@ -231,15 +249,22 @@ class Emoji(_EmojiTag, AssetMixin): The new emoji name. roles: List[:class:`~discord.abc.Snowflake`] A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. + + This does not apply if :meth:`.is_application_owned` is ``True``. + reason: Optional[:class:`str`] The reason for editing this emoji. Shows up on the audit log. + This does not apply if :meth:`.is_application_owned` is ``True``. + Raises ------- Forbidden You are not allowed to edit emojis. HTTPException An error occurred editing the emoji. + MissingApplicationID + The emoji is owned by an application but the application ID is missing Returns -------- @@ -253,5 +278,25 @@ class Emoji(_EmojiTag, AssetMixin): if roles is not MISSING: payload['roles'] = [role.id for role in roles] + if self.is_application_owned(): + application_id = self._state.application_id + if application_id is None: + raise MissingApplicationID + + payload.pop('roles', None) + data = await self._state.http.edit_application_emoji( + application_id, + self.id, + payload=payload, + ) + return Emoji(guild=Object(0), data=data, state=self._state) + data = await self._state.http.edit_custom_emoji(self.guild_id, self.id, payload=payload, reason=reason) return Emoji(guild=self.guild, data=data, state=self._state) # type: ignore # if guild is None, the http request would have failed + + def is_application_owned(self) -> bool: + """:class:`bool`: Whether the emoji is owned by an application. + + .. versionadded:: 2.5 + """ + return self.guild_id == 0 diff --git a/discord/http.py b/discord/http.py index e1bb04483..6230f9b1d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2515,7 +2515,7 @@ class HTTPClient: ), ) - # Misc + # Application def application_info(self) -> Response[appinfo.AppInfo]: return self.request(Route('GET', '/oauth2/applications/@me')) @@ -2536,6 +2536,59 @@ class HTTPClient: payload = {k: v for k, v in payload.items() if k in valid_keys} return self.request(Route('PATCH', '/applications/@me'), json=payload, reason=reason) + def get_application_emojis(self, application_id: Snowflake) -> Response[appinfo.ListAppEmojis]: + return self.request(Route('GET', '/applications/{application_id}/emojis', application_id=application_id)) + + def get_application_emoji(self, application_id: Snowflake, emoji_id: Snowflake) -> Response[emoji.Emoji]: + return self.request( + Route( + 'GET', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + ) + + def create_application_emoji( + self, + application_id: Snowflake, + name: str, + image: str, + ) -> Response[emoji.Emoji]: + payload = { + 'name': name, + 'image': image, + } + + return self.request( + Route('POST', '/applications/{application_id}/emojis', application_id=application_id), json=payload + ) + + def edit_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + *, + payload: Dict[str, Any], + ) -> Response[emoji.Emoji]: + r = Route( + 'PATCH', '/applications/{application_id}/emojis/{emoji_id}', application_id=application_id, emoji_id=emoji_id + ) + return self.request(r, json=payload) + + def delete_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + ) -> Response[None]: + return self.request( + Route( + 'DELETE', + '/applications/{application_id}/emojis/{emoji_id}', + application_id=application_id, + emoji_id=emoji_id, + ) + ) + + # Poll + def get_poll_answer_voters( self, channel_id: Snowflake, @@ -2573,6 +2626,8 @@ class HTTPClient: ) ) + # Misc + async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: try: data = await self.request(Route('GET', '/gateway')) diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index ae7fc7e0d..7cca955b7 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -30,6 +30,7 @@ from typing_extensions import NotRequired from .user import User from .team import Team from .snowflake import Snowflake +from .emoji import Emoji class InstallParams(TypedDict): @@ -79,3 +80,7 @@ class PartialAppInfo(BaseAppInfo, total=False): class GatewayAppInfo(TypedDict): id: Snowflake flags: int + + +class ListAppEmojis(TypedDict): + items: List[Emoji] From a08f7a14fff66e0bc532fca15275ee8de0f542ac Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 29 Aug 2024 03:04:14 -0400 Subject: [PATCH 22/29] Add a warning if interaction endpoint URL is set on login --- discord/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/discord/client.py b/discord/client.py index 1d2c2a326..50f76d5a2 100644 --- a/discord/client.py +++ b/discord/client.py @@ -636,6 +636,11 @@ class Client: if self._connection.application_id is None: self._connection.application_id = self._application.id + if self._application.interactions_endpoint_url is not None: + _log.warning( + 'Application has an interaction endpoint URL set, this means registered components and app commands will not be received by the library.' + ) + if not self._connection.application_flags: self._connection.application_flags = self._application.flags From 6d8198126a34b33b38a7a32391065f8dd5e2fab7 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 29 Aug 2024 03:05:35 -0400 Subject: [PATCH 23/29] Remove aiodns from being used on Windows --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d36c85c82..596e6ef08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ docs = [ ] speed = [ "orjson>=3.5.4", - "aiodns>=1.1", + "aiodns>=1.1; sys_platform != 'win32'", "Brotli", "cchardet==2.1.7; python_version < '3.10'", ] From d3e63a0162a7a0db3b6d822e596c4745af875c2c Mon Sep 17 00:00:00 2001 From: owocado <24418520+owocado@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:38:00 +0530 Subject: [PATCH 24/29] Fix Guild.invites_paused method --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 07a072d1f..9bdcda129 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4463,7 +4463,7 @@ class Guild(Hashable): .. versionadded:: 2.4 """ if not self.invites_paused_until: - return False + return 'INVITES_DISABLED' in self.features return self.invites_paused_until > utils.utcnow() @@ -4473,7 +4473,7 @@ class Guild(Hashable): .. versionadded:: 2.4 """ if not self.dms_paused_until: - return 'INVITES_DISABLED' in self.features + return False return self.dms_paused_until > utils.utcnow() From dee5bf65c63fc2feca00d32e5f4a9fa293636291 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 29 Aug 2024 16:15:14 -0400 Subject: [PATCH 25/29] Update MemberFlags to have newest values --- discord/flags.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/discord/flags.py b/discord/flags.py index 3d31e3a58..583f98c34 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -2032,6 +2032,48 @@ class MemberFlags(BaseFlags): """:class:`bool`: Returns ``True`` if the member has started onboarding.""" return 1 << 3 + @flag_value + def guest(self): + """:class:`bool`: Returns ``True`` if the member is a guest and can only access + the voice channel they were invited to. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def started_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has started Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 5 + + @flag_value + def completed_home_actions(self): + """:class:`bool`: Returns ``True`` if the member has completed Server Guide new member actions. + + .. versionadded:: 2.5 + """ + return 1 << 6 + + @flag_value + def automod_quarantined_username(self): + """:class:`bool`: Returns ``True`` if the member's username, nickname, or global name has been + blocked by AutoMod. + + .. versionadded:: 2.5 + """ + return 1 << 7 + + @flag_value + def dm_settings_upsell_acknowledged(self): + """:class:`bool`: Returns ``True`` if the member has dismissed the DM settings upsell. + + .. versionadded:: 2.5 + """ + return 1 << 9 + @fill_with_flags() class AttachmentFlags(BaseFlags): From 733c583b7250f34b657986f834f8fc7c461fcf15 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:29:05 -0400 Subject: [PATCH 26/29] Remove _get_poll lookup in Message constructor This was triggering a terrible performance regression for no good reason for all created messages that didn't have a poll, which is essentially 99.99% of messages leading to MESSAGE_CREATE dispatches having degraded performance. --- discord/message.py | 2 +- discord/state.py | 6 ------ discord/webhook/async_.py | 5 ----- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/discord/message.py b/discord/message.py index 0247ee8c1..2feeff2a6 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1849,7 +1849,7 @@ class Message(PartialMessage, Hashable): try: self.poll = Poll._from_data(data=data['poll'], message=self, state=state) except KeyError: - self.poll = state._get_poll(self.id) + pass try: # if the channel doesn't have a guild attribute, we handle that diff --git a/discord/state.py b/discord/state.py index db0395266..6279f14bf 100644 --- a/discord/state.py +++ b/discord/state.py @@ -510,12 +510,6 @@ class ConnectionState(Generic[ClientT]): def _get_message(self, msg_id: Optional[int]) -> Optional[Message]: return utils.find(lambda m: m.id == msg_id, reversed(self._messages)) if self._messages else None - def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: - message = self._get_message(msg_id) - if not message: - return - return message.poll - def _add_guild_from_data(self, data: GuildPayload) -> Guild: guild = Guild(data=data, state=self) self._add_guild(guild) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 91b021a5e..2d9856ae3 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -721,11 +721,6 @@ class _WebhookState: return self._parent._get_guild(guild_id) return None - def _get_poll(self, msg_id: Optional[int]) -> Optional[Poll]: - if self._parent is not None: - return self._parent._get_poll(msg_id) - return None - def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser: if self._parent is not None: return self._parent.store_user(data, cache=cache) From 66d74054ddd0552505a705a8af850b16ec5ea0a5 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:32:52 -0400 Subject: [PATCH 27/29] Remove outdated leftover comment about polls --- discord/message.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index 2feeff2a6..76127f869 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1843,8 +1843,6 @@ class Message(PartialMessage, Hashable): self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] - # This updates the poll so it has the counts, if the message - # was previously cached. self.poll: Optional[Poll] = None try: self.poll = Poll._from_data(data=data['poll'], message=self, state=state) From df4b1c88df741b439e97049e5c92feb969bdd5d3 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 31 Aug 2024 08:51:56 -0400 Subject: [PATCH 28/29] Move MissingApplicationID to top-level discord namespace --- discord/__init__.py | 6 ++++++ discord/app_commands/errors.py | 20 +------------------- discord/client.py | 2 +- discord/emoji.py | 2 +- discord/errors.py | 25 +++++++++++++++++++++++++ discord/sku.py | 2 +- docs/api.rst | 3 +++ docs/interactions/api.rst | 5 +---- 8 files changed, 39 insertions(+), 26 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 765719b68..2cf64c934 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -84,4 +84,10 @@ version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel= logging.getLogger(__name__).addHandler(logging.NullHandler()) +# This is a backwards compatibility hack and should be removed in v3 +# Essentially forcing the exception to have different base classes +# In the future, this should only inherit from ClientException +if len(MissingApplicationID.__bases__) == 1: + MissingApplicationID.__bases__ = (app_commands.AppCommandError, ClientException) + del logging, NamedTuple, Literal, VersionInfo diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index abdc9f2f0..2efb4e5b0 100644 --- a/discord/app_commands/errors.py +++ b/discord/app_commands/errors.py @@ -27,7 +27,7 @@ from __future__ import annotations from typing import Any, TYPE_CHECKING, List, Optional, Sequence, Union from ..enums import AppCommandOptionType, AppCommandType, Locale -from ..errors import DiscordException, HTTPException, _flatten_error_dict +from ..errors import DiscordException, HTTPException, _flatten_error_dict, MissingApplicationID as MissingApplicationID from ..utils import _human_join __all__ = ( @@ -59,11 +59,6 @@ if TYPE_CHECKING: CommandTypes = Union[Command[Any, ..., Any], Group, ContextMenu] -APP_ID_NOT_FOUND = ( - 'Client does not have an application_id set. Either the function was called before on_ready ' - 'was called or application_id was not passed to the Client constructor.' -) - class AppCommandError(DiscordException): """The base exception type for all application command related errors. @@ -422,19 +417,6 @@ class CommandSignatureMismatch(AppCommandError): super().__init__(msg) -class MissingApplicationID(AppCommandError): - """An exception raised when the client does not have an application ID set. - An application ID is required for syncing application commands. - - This inherits from :exc:`~discord.app_commands.AppCommandError`. - - .. versionadded:: 2.0 - """ - - def __init__(self, message: Optional[str] = None): - super().__init__(message or APP_ID_NOT_FOUND) - - def _get_command_error( index: str, inner: Any, diff --git a/discord/client.py b/discord/client.py index 50f76d5a2..2ca8c2ae0 100644 --- a/discord/client.py +++ b/discord/client.py @@ -84,7 +84,7 @@ if TYPE_CHECKING: from typing_extensions import Self from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime - from .app_commands import Command, ContextMenu, MissingApplicationID + from .app_commands import Command, ContextMenu from .automod import AutoModAction, AutoModRule from .channel import DMChannel, GroupChannel from .ext.commands import AutoShardedBot, Bot, Context, CommandError diff --git a/discord/emoji.py b/discord/emoji.py index e011495fd..74f344acc 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -29,7 +29,7 @@ from .asset import Asset, AssetMixin from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag, PartialEmoji from .user import User -from .app_commands.errors import MissingApplicationID +from .errors import MissingApplicationID from .object import Object # fmt: off diff --git a/discord/errors.py b/discord/errors.py index 6035ace7c..a40842578 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -47,6 +47,12 @@ __all__ = ( 'ConnectionClosed', 'PrivilegedIntentsRequired', 'InteractionResponded', + 'MissingApplicationID', +) + +APP_ID_NOT_FOUND = ( + 'Client does not have an application_id set. Either the function was called before on_ready ' + 'was called or application_id was not passed to the Client constructor.' ) @@ -278,3 +284,22 @@ class InteractionResponded(ClientException): def __init__(self, interaction: Interaction): self.interaction: Interaction = interaction super().__init__('This interaction has already been responded to before') + + +class MissingApplicationID(ClientException): + """An exception raised when the client does not have an application ID set. + + An application ID is required for syncing application commands and various + other application tasks such as SKUs or application emojis. + + This inherits from :exc:`~discord.app_commands.AppCommandError` + and :class:`~discord.ClientException`. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.5 + This is now exported to the ``discord`` namespace and now inherits from :class:`~discord.ClientException`. + """ + + def __init__(self, message: Optional[str] = None): + super().__init__(message or APP_ID_NOT_FOUND) diff --git a/discord/sku.py b/discord/sku.py index 2af171c1d..e8780399c 100644 --- a/discord/sku.py +++ b/discord/sku.py @@ -28,7 +28,7 @@ from __future__ import annotations from typing import Optional, TYPE_CHECKING from . import utils -from .app_commands import MissingApplicationID +from .errors import MissingApplicationID from .enums import try_enum, SKUType, EntitlementType from .flags import SKUFlags diff --git a/docs/api.rst b/docs/api.rst index 41cf6549d..e415ea8ce 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5440,6 +5440,8 @@ The following exceptions are thrown by the library. .. autoexception:: InteractionResponded +.. autoexception:: MissingApplicationID + .. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusNotLoaded @@ -5457,6 +5459,7 @@ Exception Hierarchy - :exc:`ConnectionClosed` - :exc:`PrivilegedIntentsRequired` - :exc:`InteractionResponded` + - :exc:`MissingApplicationID` - :exc:`GatewayNotFound` - :exc:`HTTPException` - :exc:`Forbidden` diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 211cd790f..aeb6a25c6 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -872,9 +872,6 @@ Exceptions .. autoexception:: discord.app_commands.CommandNotFound :members: -.. autoexception:: discord.app_commands.MissingApplicationID - :members: - .. autoexception:: discord.app_commands.CommandSyncFailure :members: @@ -899,7 +896,7 @@ Exception Hierarchy - :exc:`~discord.app_commands.CommandAlreadyRegistered` - :exc:`~discord.app_commands.CommandSignatureMismatch` - :exc:`~discord.app_commands.CommandNotFound` - - :exc:`~discord.app_commands.MissingApplicationID` + - :exc:`~discord.MissingApplicationID` - :exc:`~discord.app_commands.CommandSyncFailure` - :exc:`~discord.HTTPException` - :exc:`~discord.app_commands.CommandSyncFailure` From 59f877fcf013c4ddeeb2b39fc21f03e76f995461 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 2 Sep 2024 10:53:31 -0400 Subject: [PATCH 29/29] Fix and add test for missing discord.Permission bits --- discord/permissions.py | 2 +- tests/test_permissions_all.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/test_permissions_all.py diff --git a/discord/permissions.py b/discord/permissions.py index 17c7b38c9..b553e2578 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -187,7 +187,7 @@ class Permissions(BaseFlags): permissions set to ``True``. """ # Some of these are 0 because we don't want to set unnecessary bits - return cls(0b0000_0000_0000_0010_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + return cls(0b0000_0000_0000_0110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) @classmethod def _timeout_mask(cls) -> int: diff --git a/tests/test_permissions_all.py b/tests/test_permissions_all.py new file mode 100644 index 000000000..883dc1b63 --- /dev/null +++ b/tests/test_permissions_all.py @@ -0,0 +1,7 @@ +import discord + +from functools import reduce +from operator import or_ + +def test_permissions_all(): + assert discord.Permissions.all().value == reduce(or_, discord.Permissions.VALID_FLAGS.values())