diff --git a/discord/app_commands/errors.py b/discord/app_commands/errors.py index dc63f10e8..abdc9f2f0 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,10 +497,9 @@ 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()) + if isinstance(errors, dict): + messages.extend(f'{indentation} {k}: {v}' for k, v in errors.items()) class CommandSyncFailure(AppCommandError, HTTPException): 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/client.py b/discord/client.py index a91be7160..1d2c2a326 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, @@ -359,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 @@ -2919,6 +2932,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| @@ -3039,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/embeds.py b/discord/embeds.py index 6a79fef71..258ef0dfd 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 ----------- @@ -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`. """ @@ -452,13 +453,11 @@ class Embed: This function returns the class instance to allow for fluent-style chaining. - .. versionchanged:: 1.4 - Passing ``None`` removes the thumbnail. - 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`. """ 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/guild.py b/discord/guild.py index 0ca06ee1e..058ce323f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3422,6 +3422,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, @@ -4418,6 +4449,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. @@ -4434,6 +4487,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() diff --git a/discord/http.py b/discord/http.py index abf6a9b23..6b755b028 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 @@ -93,6 +92,7 @@ if TYPE_CHECKING: welcome_screen, sku, poll, + voice, ) from .types.snowflake import Snowflake, SnowflakeList @@ -798,13 +798,13 @@ 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, 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() @@ -1148,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( @@ -1611,6 +1617,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')) @@ -1858,6 +1867,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]: @@ -2505,7 +2517,7 @@ class HTTPClient: ), ) - # Misc + # Application def application_info(self) -> Response[appinfo.AppInfo]: return self.request(Route('GET', '/oauth2/applications/@me')) @@ -2526,6 +2538,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, @@ -2563,6 +2628,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/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. 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 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, diff --git a/discord/sticker.py b/discord/sticker.py index 30eb62c70..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, ) @@ -203,7 +201,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'' @@ -258,8 +259,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` @@ -352,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. @@ -364,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): diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index e291babfa..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): @@ -45,6 +46,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] @@ -78,3 +80,7 @@ class PartialAppInfo(BaseAppInfo, total=False): class GatewayAppInfo(TypedDict): id: Snowflake flags: int + + +class ListAppEmojis(TypedDict): + items: List[Emoji] 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/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) 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: 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'