From 3db6ae379a8c73a4b64537ed45c2aa8be9891ae8 Mon Sep 17 00:00:00 2001 From: UK <41271523+NeloBlivion@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:42:12 +0100 Subject: [PATCH] feat: app emojis (#2501) * add routes * unfinished methods, needs rework * style(pre-commit): auto fixes from pre-commit.com hooks * new classes * style(pre-commit): auto fixes from pre-commit.com hooks * refinements * style(pre-commit): auto fixes from pre-commit.com hooks * fix kwargs * _state -> _connection * style(pre-commit): auto fixes from pre-commit.com hooks * cache on ready * full cache * remove delete reason * style(pre-commit): auto fixes from pre-commit.com hooks * adjust slots * style(pre-commit): auto fixes from pre-commit.com hooks * Update discord/emoji.py Signed-off-by: plun1331 * style(pre-commit): auto fixes from pre-commit.com hooks * update all references to the Emoji class * style(pre-commit): auto fixes from pre-commit.com hooks * misc * cl * style(pre-commit): auto fixes from pre-commit.com hooks * add deprecation --------- Signed-off-by: plun1331 Signed-off-by: UK <41271523+NeloBlivion@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil Co-authored-by: plun1331 --- CHANGELOG.md | 8 + discord/abc.py | 10 +- discord/audit_logs.py | 6 +- discord/channel.py | 12 +- discord/client.py | 149 +++++++++++++++- discord/components.py | 8 +- discord/emoji.py | 273 +++++++++++++++++++++++------- discord/ext/commands/converter.py | 8 +- discord/ext/pages/pagination.py | 20 ++- discord/flags.py | 2 +- discord/guild.py | 42 ++--- discord/http.py | 69 ++++++++ discord/message.py | 20 +-- discord/onboarding.py | 8 +- discord/poll.py | 23 ++- discord/reaction.py | 8 +- discord/state.py | 45 ++++- discord/ui/button.py | 18 +- discord/ui/select.py | 8 +- discord/welcome_screen.py | 6 +- docs/api/enums.rst | 4 +- docs/api/events.rst | 6 +- docs/api/models.rst | 10 +- docs/ext/commands/commands.rst | 4 +- docs/faq.rst | 2 +- 25 files changed, 595 insertions(+), 174 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3405068a18..7da04a101a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ These changes are available on the `master` branch, but have not yet been releas `tags`. ([#2520](https://github.com/Pycord-Development/pycord/pull/2520)) - Added `Member.guild_banner` and `Member.display_banner` properties. ([#2556](https://github.com/Pycord-Development/pycord/pull/2556)) +- Added support for Application Emojis. + ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) +- Added `cache_app_emojis` parameter to `Client`. + ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Added `elapsed` method to `VoiceClient`. ([#2587](https://github.com/Pycord-Development/pycord/pull/2587/)) - Added optional `filter` parameter to `utils.basic_autocomplete()`. @@ -52,6 +56,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2496](https://github.com/Pycord-Development/pycord/pull/2496)) - ⚠️ **Removed support for Python 3.8.** ([#2521](https://github.com/Pycord-Development/pycord/pull/2521)) +- `Emoji` has been renamed to `GuildEmoji`. + ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Replaced audioop (deprecated module) implementation of `PCMVolumeTransformer.read` method with a pure Python equivalent. ([#2176](https://github.com/Pycord-Development/pycord/pull/2176)) @@ -60,6 +66,8 @@ These changes are available on the `master` branch, but have not yet been releas - Deprecated `AppInfo.summary` in favor of `AppInfo.description`. ([#2520](https://github.com/Pycord-Development/pycord/pull/2520)) +- Deprecated `Emoji` in favor of `GuildEmoji` + ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) ## [2.6.1] - 2024-09-15 diff --git a/discord/abc.py b/discord/abc.py index d699f44702..7e9d462b89 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -513,7 +513,9 @@ async def _edit( except KeyError: pass else: - if isinstance(default_reaction_emoji, _EmojiTag): # Emoji, PartialEmoji + if isinstance( + default_reaction_emoji, _EmojiTag + ): # GuildEmoji, PartialEmoji default_reaction_emoji = default_reaction_emoji._to_partial() elif isinstance(default_reaction_emoji, int): default_reaction_emoji = PartialEmoji( @@ -523,7 +525,7 @@ async def _edit( default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) else: raise InvalidArgument( - "default_reaction_emoji must be of type: Emoji | int | str" + "default_reaction_emoji must be of type: GuildEmoji | int | str" ) options["default_reaction_emoji"] = ( @@ -1792,7 +1794,7 @@ def can_send(self, *objects) -> bool: "Message": "send_messages", "Embed": "embed_links", "File": "attach_files", - "Emoji": "use_external_emojis", + "GuildEmoji": "use_external_emojis", "GuildSticker": "use_external_stickers", } # Can't use channel = await self._get_channel() since its async @@ -1817,7 +1819,7 @@ def can_send(self, *objects) -> bool: mapping.get(type(obj).__name__) or mapping[obj.__name__] ) - if type(obj).__name__ == "Emoji": + if type(obj).__name__ == "GuildEmoji": if ( obj._to_partial().is_unicode_emoji or obj.guild_id == channel.guild.id diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 7497e8c37a..e2ff277dfe 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -47,7 +47,7 @@ import datetime from . import abc - from .emoji import Emoji + from .emoji import GuildEmoji from .guild import Guild from .member import Member from .role import Role @@ -617,7 +617,7 @@ def target( | User | Role | Invite - | Emoji + | GuildEmoji | StageInstance | GuildSticker | Thread @@ -689,7 +689,7 @@ def _convert_target_invite(self, target_id: int) -> Invite: pass return obj - def _convert_target_emoji(self, target_id: int) -> Emoji | Object: + def _convert_target_emoji(self, target_id: int) -> GuildEmoji | Object: return self._state.get_emoji(target_id) or Object(id=target_id) def _convert_target_message(self, target_id: int) -> Member | User | None: diff --git a/discord/channel.py b/discord/channel.py index 27230a380f..d70083d9f7 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -32,7 +32,7 @@ from . import utils from .asset import Asset -from .emoji import Emoji +from .emoji import GuildEmoji from .enums import ( ChannelType, EmbeddedActivity, @@ -143,7 +143,7 @@ def __init__( self.emoji = PartialEmoji.from_str(emoji) else: raise TypeError( - "emoji must be a Emoji, PartialEmoji, or str and not" + "emoji must be a GuildEmoji, PartialEmoji, or str and not" f" {emoji.__class__!r}" ) @@ -1018,7 +1018,7 @@ class ForumChannel(_TextChannel): The initial slowmode delay to set on newly created threads in this channel. .. versionadded:: 2.3 - default_reaction_emoji: Optional[:class:`str` | :class:`discord.Emoji`] + default_reaction_emoji: Optional[:class:`str` | :class:`discord.GuildEmoji`] The default forum reaction emoji. .. versionadded:: 2.5 @@ -1087,7 +1087,7 @@ async def edit( default_auto_archive_duration: ThreadArchiveDuration = ..., default_thread_slowmode_delay: int = ..., default_sort_order: SortOrder = ..., - default_reaction_emoji: Emoji | int | str | None = ..., + default_reaction_emoji: GuildEmoji | int | str | None = ..., available_tags: list[ForumTag] = ..., require_tag: bool = ..., overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., @@ -1138,10 +1138,10 @@ async def edit(self, *, reason=None, **options): The default sort order type to use to order posts in this channel. .. versionadded:: 2.3 - default_reaction_emoji: Optional[:class:`discord.Emoji` | :class:`int` | :class:`str`] + default_reaction_emoji: Optional[:class:`discord.GuildEmoji` | :class:`int` | :class:`str`] The default reaction emoji. Can be a unicode emoji or a custom emoji in the forms: - :class:`Emoji`, snowflake ID, string representation (eg. ''). + :class:`GuildEmoji`, snowflake ID, string representation (eg. ''). .. versionadded:: 2.5 available_tags: List[:class:`ForumTag`] diff --git a/discord/client.py b/discord/client.py index 7f13696f6f..8ece21cf94 100644 --- a/discord/client.py +++ b/discord/client.py @@ -41,7 +41,7 @@ from .application_role_connection import ApplicationRoleConnectionMetadata from .backoff import ExponentialBackoff from .channel import PartialMessageable, _threaded_channel_factory -from .emoji import Emoji +from .emoji import AppEmoji, GuildEmoji from .enums import ChannelType, Status from .errors import * from .flags import ApplicationFlags, Intents @@ -199,6 +199,16 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 + cache_app_emojis: :class:`bool` + Whether to automatically fetch and cache the application's emojis on startup and when fetching. Defaults to ``False``. + + .. warning:: + + There are no events related to application emojis - if any are created/deleted on the + Developer Dashboard while the client is running, the cache will not be updated until you manually + run :func:`fetch_emojis`. + + .. versionadded:: 2.7 Attributes ----------- @@ -330,10 +340,30 @@ def guilds(self) -> list[Guild]: return self._connection.guilds @property - def emojis(self) -> list[Emoji]: - """The emojis that the connected client has.""" + def emojis(self) -> list[GuildEmoji | AppEmoji]: + """The emojis that the connected client has. + + .. note:: + + This only includes the application's emojis if `cache_app_emojis` is ``True``. + """ return self._connection.emojis + @property + def guild_emojis(self) -> list[GuildEmoji]: + """The :class:`~discord.GuildEmoji` that the connected client has.""" + return [e for e in self.emojis if isinstance(e, GuildEmoji)] + + @property + def app_emojis(self) -> list[AppEmoji]: + """The :class:`~discord.AppEmoji` that the connected client has. + + .. note:: + + This is only available if `cache_app_emojis` is ``True``. + """ + return [e for e in self.emojis if isinstance(e, AppEmoji)] + @property def stickers(self) -> list[GuildSticker]: """The stickers that the connected client has. @@ -994,7 +1024,7 @@ def get_user(self, id: int, /) -> User | None: """ return self._connection.get_user(id) - def get_emoji(self, id: int, /) -> Emoji | None: + def get_emoji(self, id: int, /) -> GuildEmoji | AppEmoji | None: """Returns an emoji with the given ID. Parameters @@ -1004,7 +1034,7 @@ def get_emoji(self, id: int, /) -> Emoji | None: Returns ------- - Optional[:class:`.Emoji`] + Optional[:class:`.GuildEmoji` | :class:`.AppEmoji`] The custom emoji or ``None`` if not found. """ return self._connection.get_emoji(id) @@ -2130,3 +2160,112 @@ def store_url(self) -> str: .. versionadded:: 2.6 """ return f"https://discord.com/application-directory/{self.application_id}/store" + + async def fetch_emojis(self) -> list[AppEmoji]: + r"""|coro| + + Retrieves all custom :class:`AppEmoji`\s from the application. + + Raises + --------- + HTTPException + An error occurred fetching the emojis. + + Returns + -------- + List[:class:`AppEmoji`] + The retrieved emojis. + """ + data = await self._connection.http.get_all_application_emojis( + self.application_id + ) + return [ + self._connection.maybe_store_app_emoji(self.application_id, d) + for d in data["items"] + ] + + async def fetch_emoji(self, emoji_id: int, /) -> AppEmoji: + """|coro| + + Retrieves a custom :class:`AppEmoji` from the application. + + Parameters + ---------- + emoji_id: :class:`int` + The emoji's ID. + + Returns + ------- + :class:`AppEmoji` + The retrieved emoji. + + Raises + ------ + NotFound + The emoji requested could not be found. + HTTPException + An error occurred fetching the emoji. + """ + data = await self._connection.http.get_application_emoji( + self.application_id, emoji_id + ) + return self._connection.maybe_store_app_emoji(self.application_id, data) + + async def create_emoji( + self, + *, + name: str, + image: bytes, + ) -> AppEmoji: + r"""|coro| + + Creates a custom :class:`AppEmoji` for the application. + + There is currently a limit of 2000 emojis per application. + + 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 + ------- + HTTPException + An error occurred creating an emoji. + + Returns + -------- + :class:`AppEmoji` + The created emoji. + """ + + img = utils._bytes_to_base64_data(image) + data = await self._connection.http.create_application_emoji( + self.application_id, name, img + ) + return self._connection.maybe_store_app_emoji(self.application_id, data) + + async def delete_emoji(self, emoji: Snowflake) -> None: + """|coro| + + Deletes the custom :class:`AppEmoji` from the application. + + Parameters + ---------- + emoji: :class:`abc.Snowflake` + The emoji you are deleting. + + Raises + ------ + HTTPException + An error occurred deleting the emoji. + """ + + await self._connection.http.delete_application_emoji( + self.application_id, emoji.id + ) + if self._connection.cache_app_emojis and self._connection.get_emoji(emoji.id): + self._connection.remove_emoji(emoji) diff --git a/discord/components.py b/discord/components.py index d85fc4b07c..c80eb5a57c 100644 --- a/discord/components.py +++ b/discord/components.py @@ -32,7 +32,7 @@ from .utils import MISSING, get_slots if TYPE_CHECKING: - from .emoji import Emoji + from .emoji import AppEmoji, GuildEmoji from .types.components import ActionRow as ActionRowPayload from .types.components import ButtonComponent as ButtonComponentPayload from .types.components import Component as ComponentPayload @@ -412,7 +412,7 @@ def __init__( label: str, value: str = MISSING, description: str | None = None, - emoji: str | Emoji | PartialEmoji | None = None, + emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, default: bool = False, ) -> None: if len(label) > 100: @@ -444,7 +444,7 @@ def __str__(self) -> str: return base @property - def emoji(self) -> str | Emoji | PartialEmoji | None: + def emoji(self) -> str | GuildEmoji | AppEmoji | PartialEmoji | None: """The emoji of the option, if available.""" return self._emoji @@ -457,7 +457,7 @@ def emoji(self, value) -> None: value = value._to_partial() else: raise TypeError( - "expected emoji to be str, Emoji, or PartialEmoji not" + "expected emoji to be str, GuildEmoji, AppEmoji, or PartialEmoji, not" f" {value.__class__}" ) diff --git a/discord/emoji.py b/discord/emoji.py index cbb51eee57..417751fce8 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -32,7 +32,11 @@ from .user import User from .utils import MISSING, SnowflakeList, snowflake_time -__all__ = ("Emoji",) +__all__ = ( + "Emoji", + "GuildEmoji", + "AppEmoji", +) if TYPE_CHECKING: from datetime import datetime @@ -44,8 +48,71 @@ from .types.emoji import Emoji as EmojiPayload -class Emoji(_EmojiTag, AssetMixin): - """Represents a custom emoji. +class BaseEmoji(_EmojiTag, AssetMixin): + + __slots__: tuple[str, ...] = ( + "require_colons", + "animated", + "managed", + "id", + "name", + "_state", + "user", + "available", + ) + + def __init__(self, *, state: ConnectionState, data: EmojiPayload): + self._state: ConnectionState = state + self._from_data(data) + + def _from_data(self, emoji: EmojiPayload): + self.require_colons: bool = emoji.get("require_colons", False) + self.managed: bool = emoji.get("managed", False) + self.id: int = int(emoji["id"]) # type: ignore + self.name: str = emoji["name"] # type: ignore + self.animated: bool = emoji.get("animated", False) + self.available: bool = emoji.get("available", True) + user = emoji.get("user") + self.user: User | None = User(state=self._state, data=user) if user else None + + def _to_partial(self) -> PartialEmoji: + return PartialEmoji(name=self.name, animated=self.animated, id=self.id) + + def __iter__(self) -> Iterator[tuple[str, Any]]: + for attr in self.__slots__: + if attr[0] != "_": + value = getattr(self, attr, None) + if value is not None: + yield attr, value + + def __str__(self) -> str: + if self.animated: + return f"" + return f"<:{self.name}:{self.id}>" + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: Any) -> bool: + return isinstance(other, _EmojiTag) and self.id == other.id + + def __hash__(self) -> int: + return self.id >> 22 + + @property + def created_at(self) -> datetime: + """Returns the emoji's creation time in UTC.""" + return snowflake_time(self.id) + + @property + def url(self) -> str: + """Returns the URL of the emoji.""" + fmt = "gif" if self.animated else "png" + return f"{Asset.BASE}/emojis/{self.id}.{fmt}" + + +class GuildEmoji(BaseEmoji): + """Represents a custom emoji in a guild. Depending on the way this object was created, some attributes can have a value of ``None``. @@ -95,72 +162,21 @@ class Emoji(_EmojiTag, AssetMixin): """ __slots__: tuple[str, ...] = ( - "require_colons", - "animated", - "managed", - "id", - "name", "_roles", "guild_id", - "_state", - "user", - "available", ) def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload): self.guild_id: int = guild.id - self._state: ConnectionState = state - self._from_data(data) - - def _from_data(self, emoji: EmojiPayload): - self.require_colons: bool = emoji.get("require_colons", False) - self.managed: bool = emoji.get("managed", False) - self.id: int = int(emoji["id"]) # type: ignore - self.name: str = emoji["name"] # type: ignore - self.animated: bool = emoji.get("animated", False) - self.available: bool = emoji.get("available", True) - self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get("roles", []))) - user = emoji.get("user") - self.user: User | None = User(state=self._state, data=user) if user else None - - def _to_partial(self) -> PartialEmoji: - return PartialEmoji(name=self.name, animated=self.animated, id=self.id) - - def __iter__(self) -> Iterator[tuple[str, Any]]: - for attr in self.__slots__: - if attr[0] != "_": - value = getattr(self, attr, None) - if value is not None: - yield attr, value - - def __str__(self) -> str: - if self.animated: - return f"" - return f"<:{self.name}:{self.id}>" + self._roles: SnowflakeList = SnowflakeList(map(int, data.get("roles", []))) + super().__init__(state=state, data=data) def __repr__(self) -> str: return ( - "" ) - def __eq__(self, other: Any) -> bool: - return isinstance(other, _EmojiTag) and self.id == other.id - - def __hash__(self) -> int: - return self.id >> 22 - - @property - def created_at(self) -> datetime: - """Returns the emoji's creation time in UTC.""" - return snowflake_time(self.id) - - @property - def url(self) -> str: - """Returns the URL of the emoji.""" - fmt = "gif" if self.animated else "png" - return f"{Asset.BASE}/emojis/{self.id}.{fmt}" - @property def roles(self) -> list[Role]: """A :class:`list` of roles that is allowed to use this emoji. @@ -221,7 +237,7 @@ async def edit( name: str = MISSING, roles: list[Snowflake] = MISSING, reason: str | None = None, - ) -> Emoji: + ) -> GuildEmoji: r"""|coro| Edits the custom emoji. @@ -250,7 +266,7 @@ async def edit( Returns -------- - :class:`Emoji` + :class:`GuildEmoji` The newly updated emoji. """ @@ -263,4 +279,141 @@ async def edit( 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) + return GuildEmoji(guild=self.guild, data=data, state=self._state) + + +Emoji = GuildEmoji + + +class AppEmoji(BaseEmoji): + """Represents a custom emoji from an application. + + Depending on the way this object was created, some attributes can + have a value of ``None``. + + .. versionadded:: 2.7 + + .. container:: operations + + .. describe:: x == y + + Checks if two emoji are the same. + + .. describe:: x != y + + Checks if two emoji are not the same. + + .. describe:: hash(x) + + Return the emoji's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(field, value)`` pairs. This allows this class + to be used as an iterable in list/dict/etc constructions. + + .. describe:: str(x) + + Returns the emoji rendered for discord. + + Attributes + ---------- + name: :class:`str` + The name of the emoji. + id: :class:`int` + The emoji's ID. + require_colons: :class:`bool` + If colons are required to use this emoji in the client (:PJSalt: vs PJSalt). + animated: :class:`bool` + Whether an emoji is animated or not. + managed: :class:`bool` + If this emoji is managed by a Twitch integration. + application_id: Optional[:class:`int`] + The application ID the emoji belongs to, if available. + available: :class:`bool` + Whether the emoji is available for use. + user: Optional[:class:`User`] + The user that created the emoji. + """ + + __slots__: tuple[str, ...] = ("application_id",) + + def __init__( + self, *, application_id: int, state: ConnectionState, data: EmojiPayload + ): + self.application_id: int = application_id + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return "" + + @property + def guild(self) -> Guild: + """The guild this emoji belongs to. This is always `None` for :class:`AppEmoji`.""" + return None + + @property + def roles(self) -> list[Role]: + """A :class:`list` of roles that is allowed to use this emoji. This is always empty for :class:`AppEmoji`.""" + return [] + + def is_usable(self) -> bool: + """Whether the bot can use this emoji.""" + return self.application_id == self._state.application_id + + async def delete(self) -> None: + """|coro| + + Deletes the application emoji. + + You must own the emoji to do this. + + Raises + ------ + Forbidden + You are not allowed to delete the emoji. + HTTPException + An error occurred deleting the emoji. + """ + + await self._state.http.delete_application_emoji(self.application_id, self.id) + if self._state.cache_app_emojis and self._state.get_emoji(self.id): + self._state._remove_emoji(self) + + async def edit( + self, + *, + name: str = MISSING, + ) -> AppEmoji: + r"""|coro| + + Edits the application emoji. + + You must own the emoji to do this. + + Parameters + ----------- + name: :class:`str` + The new emoji name. + + Raises + ------- + Forbidden + You are not allowed to edit the emoji. + HTTPException + An error occurred editing the emoji. + + Returns + -------- + :class:`AppEmoji` + The newly updated emoji. + """ + + payload = {} + if name is not MISSING: + payload["name"] = name + + data = await self._state.http.edit_application_emoji( + self.application_id, self.id, payload=payload + ) + return self._state.maybe_store_app_emoji(self.application_id, data) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 3ed53fa1d0..e60ac89b34 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -805,8 +805,8 @@ async def convert(self, ctx: Context, argument: str) -> discord.Guild: return result -class EmojiConverter(IDConverter[discord.Emoji]): - """Converts to a :class:`~discord.Emoji`. +class EmojiConverter(IDConverter[discord.GuildEmoji]): + """Converts to a :class:`~discord.GuildEmoji`. All lookups are done for the local guild first, if available. If that lookup fails, then it checks the client's global cache. @@ -821,7 +821,7 @@ class EmojiConverter(IDConverter[discord.Emoji]): Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument` """ - async def convert(self, ctx: Context, argument: str) -> discord.Emoji: + async def convert(self, ctx: Context, argument: str) -> discord.GuildEmoji: match = self._get_id_match(argument) or re.match( r"$", argument ) @@ -1111,7 +1111,7 @@ def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: discord.Colour: ColourConverter, discord.VoiceChannel: VoiceChannelConverter, discord.StageChannel: StageChannelConverter, - discord.Emoji: EmojiConverter, + discord.GuildEmoji: EmojiConverter, discord.PartialEmoji: PartialEmojiConverter, discord.CategoryChannel: CategoryChannelConverter, discord.ForumChannel: ForumChannelConverter, diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 884a157a92..dc99996f2a 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -54,7 +54,7 @@ class PaginatorButton(discord.ui.Button): label: :class:`str` The label shown on the button. Defaults to a capitalized version of ``button_type`` (e.g. "Next", "Prev", etc.) - emoji: Union[:class:`str`, :class:`discord.Emoji`, :class:`discord.PartialEmoji`] + emoji: Union[:class:`str`, :class:`discord.GuildEmoji`, :class:`discord.AppEmoji`, :class:`discord.PartialEmoji`] The emoji shown on the button in front of the label. disabled: :class:`bool` Whether to initially show the button as disabled. @@ -72,7 +72,9 @@ def __init__( self, button_type: str, label: str = None, - emoji: str | discord.Emoji | discord.PartialEmoji = None, + emoji: ( + str | discord.GuildEmoji | discord.AppEmoji | discord.PartialEmoji + ) = None, style: discord.ButtonStyle = discord.ButtonStyle.green, disabled: bool = False, custom_id: str = None, @@ -89,7 +91,9 @@ def __init__( ) self.button_type = button_type self.label = label if label or emoji else button_type.capitalize() - self.emoji: str | discord.Emoji | discord.PartialEmoji = emoji + self.emoji: ( + str | discord.GuildEmoji | discord.AppEmoji | discord.PartialEmoji + ) = emoji self.style = style self.disabled = disabled self.loop_label = self.label if not loop_label else loop_label @@ -242,7 +246,7 @@ class PageGroup: Also used as the SelectOption value. description: Optional[:class:`str`] The description shown on the corresponding PaginatorMenu dropdown option. - emoji: Union[:class:`str`, :class:`discord.Emoji`, :class:`discord.PartialEmoji`] + emoji: Union[:class:`str`, :class:`discord.GuildEmoji`, :class:`discord.AppEmoji`, :class:`discord.PartialEmoji`] The emoji shown on the corresponding PaginatorMenu dropdown option. default: Optional[:class:`bool`] Whether the page group should be the default page group initially shown when the paginator response is sent. @@ -278,7 +282,9 @@ def __init__( pages: list[str] | list[Page] | list[list[discord.Embed] | discord.Embed], label: str, description: str | None = None, - emoji: str | discord.Emoji | discord.PartialEmoji = None, + emoji: ( + str | discord.GuildEmoji | discord.AppEmoji | discord.PartialEmoji + ) = None, default: bool | None = None, show_disabled: bool | None = None, show_indicator: bool | None = None, @@ -294,7 +300,9 @@ def __init__( ): self.label = label self.description: str | None = description - self.emoji: str | discord.Emoji | discord.PartialEmoji = emoji + self.emoji: ( + str | discord.GuildEmoji | discord.AppEmoji | discord.PartialEmoji + ) = emoji self.pages: list[str] | list[list[discord.Embed] | discord.Embed] = pages self.default: bool | None = default self.show_disabled = show_disabled diff --git a/discord/flags.py b/discord/flags.py index 8a1ac8fd7e..1a6af50c72 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -769,7 +769,7 @@ def emojis_and_stickers(self): This also corresponds to the following attributes and classes in terms of cache: - - :class:`Emoji` + - :class:`GuildEmoji` - :class:`GuildSticker` - :meth:`Client.get_emoji` - :meth:`Client.get_sticker` diff --git a/discord/guild.py b/discord/guild.py index 881e8f1307..b1e937d07b 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -46,7 +46,7 @@ from .channel import * from .channel import _guild_channel_factory, _threaded_guild_channel_factory from .colour import Colour -from .emoji import Emoji, PartialEmoji, _EmojiTag +from .emoji import GuildEmoji, PartialEmoji, _EmojiTag from .enums import ( AuditLogAction, AutoModEventType, @@ -161,7 +161,7 @@ class Guild(Hashable): ---------- name: :class:`str` The guild name. - emojis: Tuple[:class:`Emoji`, ...] + emojis: Tuple[:class:`GuildEmoji`, ...] All emojis that the guild owns. stickers: Tuple[:class:`GuildSticker`, ...] All stickers that the guild owns. @@ -476,7 +476,7 @@ def _from_data(self, guild: GuildPayload) -> None: self._roles[role.id] = role self.mfa_level: MFALevel = guild.get("mfa_level") - self.emojis: tuple[Emoji, ...] = tuple( + self.emojis: tuple[GuildEmoji, ...] = tuple( map(lambda d: state.store_emoji(self, d), guild.get("emojis", [])) ) self.stickers: tuple[GuildSticker, ...] = tuple( @@ -1404,7 +1404,7 @@ async def create_forum_channel( slowmode_delay: int = MISSING, nsfw: bool = MISSING, overwrites: dict[Role | Member, PermissionOverwrite] = MISSING, - default_reaction_emoji: Emoji | int | str = MISSING, + default_reaction_emoji: GuildEmoji | int | str = MISSING, ) -> ForumChannel: """|coro| @@ -1446,10 +1446,10 @@ async def create_forum_channel( To mark the channel as NSFW or not. reason: Optional[:class:`str`] The reason for creating this channel. Shows up on the audit log. - default_reaction_emoji: Optional[:class:`Emoji` | :class:`int` | :class:`str`] + default_reaction_emoji: Optional[:class:`GuildEmoji` | :class:`int` | :class:`str`] The default reaction emoji. Can be a unicode emoji or a custom emoji in the forms: - :class:`Emoji`, snowflake ID, string representation (eg. ''). + :class:`GuildEmoji`, snowflake ID, string representation (eg. ''). .. versionadded:: v2.5 @@ -1502,7 +1502,9 @@ async def create_forum_channel( options["nsfw"] = nsfw if default_reaction_emoji is not MISSING: - if isinstance(default_reaction_emoji, _EmojiTag): # Emoji, PartialEmoji + if isinstance( + default_reaction_emoji, _EmojiTag + ): # GuildEmoji, PartialEmoji default_reaction_emoji = default_reaction_emoji._to_partial() elif isinstance(default_reaction_emoji, int): default_reaction_emoji = PartialEmoji( @@ -1512,7 +1514,7 @@ async def create_forum_channel( default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) else: raise InvalidArgument( - "default_reaction_emoji must be of type: Emoji | int | str" + "default_reaction_emoji must be of type: GuildEmoji | int | str" ) options["default_reaction_emoji"] = ( @@ -2662,10 +2664,10 @@ async def delete_sticker( """ await self._state.http.delete_guild_sticker(self.id, sticker.id, reason) - async def fetch_emojis(self) -> list[Emoji]: + async def fetch_emojis(self) -> list[GuildEmoji]: r"""|coro| - Retrieves all custom :class:`Emoji`\s from the guild. + Retrieves all custom :class:`GuildEmoji`\s from the guild. .. note:: @@ -2678,16 +2680,16 @@ async def fetch_emojis(self) -> list[Emoji]: Returns -------- - List[:class:`Emoji`] + List[:class:`GuildEmoji`] The retrieved emojis. """ data = await self._state.http.get_all_custom_emojis(self.id) - return [Emoji(guild=self, state=self._state, data=d) for d in data] + return [GuildEmoji(guild=self, state=self._state, data=d) for d in data] - async def fetch_emoji(self, emoji_id: int, /) -> Emoji: + async def fetch_emoji(self, emoji_id: int, /) -> GuildEmoji: """|coro| - Retrieves a custom :class:`Emoji` from the guild. + Retrieves a custom :class:`GuildEmoji` from the guild. .. note:: @@ -2701,7 +2703,7 @@ async def fetch_emoji(self, emoji_id: int, /) -> Emoji: Returns ------- - :class:`Emoji` + :class:`GuildEmoji` The retrieved emoji. Raises @@ -2712,7 +2714,7 @@ async def fetch_emoji(self, emoji_id: int, /) -> Emoji: An error occurred fetching the emoji. """ data = await self._state.http.get_custom_emoji(self.id, emoji_id) - return Emoji(guild=self, state=self._state, data=data) + return GuildEmoji(guild=self, state=self._state, data=data) async def create_custom_emoji( self, @@ -2721,10 +2723,10 @@ async def create_custom_emoji( image: bytes, roles: list[Role] = MISSING, reason: str | None = None, - ) -> Emoji: + ) -> GuildEmoji: r"""|coro| - Creates a custom :class:`Emoji` for the guild. + Creates a custom :class:`GuildEmoji` for the guild. There is currently a limit of 50 static and animated emojis respectively per guild, unless the guild has the ``MORE_EMOJI`` feature which extends the limit to 200. @@ -2753,7 +2755,7 @@ async def create_custom_emoji( Returns -------- - :class:`Emoji` + :class:`GuildEmoji` The created emoji. """ @@ -2769,7 +2771,7 @@ async def delete_emoji( ) -> None: """|coro| - Deletes the custom :class:`Emoji` from the guild. + Deletes the custom :class:`GuildEmoji` from the guild. You must have :attr:`~Permissions.manage_emojis` permission to do this. diff --git a/discord/http.py b/discord/http.py index f42bcdc233..26b584ba7e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1878,6 +1878,75 @@ def edit_custom_emoji( ) return self.request(r, json=payload, reason=reason) + def get_all_application_emojis( + self, application_id: Snowflake + ) -> Response[list[emoji.Emoji]]: + 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: bytes, + ) -> Response[emoji.Emoji]: + payload = { + "name": name, + "image": image, + } + + r = Route( + "POST", + "/applications/{application_id}/emojis", + application_id=application_id, + ) + return self.request(r, json=payload) + + def delete_application_emoji( + self, + application_id: Snowflake, + emoji_id: Snowflake, + ) -> Response[None]: + r = Route( + "DELETE", + "/applications/{application_id}/emojis/{emoji_id}", + application_id=application_id, + emoji_id=emoji_id, + ) + return self.request(r) + + 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 get_all_integrations( self, guild_id: Snowflake ) -> Response[list[integration.Integration]]: diff --git a/discord/message.py b/discord/message.py index 64df3559f3..5ebd2aa0ea 100644 --- a/discord/message.py +++ b/discord/message.py @@ -45,7 +45,7 @@ from .channel import PartialMessageable from .components import _component_factory from .embeds import Embed -from .emoji import Emoji +from .emoji import AppEmoji, GuildEmoji from .enums import ChannelType, MessageType, try_enum from .errors import InvalidArgument from .file import File @@ -93,7 +93,7 @@ from .user import User MR = TypeVar("MR", bound="MessageReference") - EmojiInputType = Union[Emoji, PartialEmoji, str] + EmojiInputType = Union[GuildEmoji, AppEmoji, PartialEmoji, str] __all__ = ( "Attachment", @@ -109,7 +109,7 @@ def convert_emoji_reaction(emoji): if isinstance(emoji, Reaction): emoji = emoji.emoji - if isinstance(emoji, Emoji): + if isinstance(emoji, (GuildEmoji, AppEmoji)): return f"{emoji.name}:{emoji.id}" if isinstance(emoji, PartialEmoji): return emoji._as_reaction() @@ -119,7 +119,7 @@ def convert_emoji_reaction(emoji): return emoji.strip("<>") raise InvalidArgument( - "emoji argument must be str, Emoji, or Reaction not" + "emoji argument must be str, GuildEmoji, AppEmoji, or Reaction not" f" {emoji.__class__.__name__}." ) @@ -1725,7 +1725,7 @@ async def add_reaction(self, emoji: EmojiInputType) -> None: Add a reaction to the message. - The emoji may be a unicode emoji or a custom guild :class:`Emoji`. + The emoji may be a unicode emoji, a custom :class:`GuildEmoji`, or an :class:`AppEmoji`. You must have the :attr:`~Permissions.read_message_history` permission to use this. If nobody else has reacted to the message using this @@ -1733,7 +1733,7 @@ async def add_reaction(self, emoji: EmojiInputType) -> None: Parameters ---------- - emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] + emoji: Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] The emoji to react with. Raises @@ -1758,7 +1758,7 @@ async def remove_reaction( Remove a reaction by the member from the message. - The emoji may be a unicode emoji or a custom guild :class:`Emoji`. + The emoji may be a unicode emoji, a custom :class:`GuildEmoji`, or an :class:`AppEmoji`. If the reaction is not your own (i.e. ``member`` parameter is not you) then the :attr:`~Permissions.manage_messages` permission is needed. @@ -1768,7 +1768,7 @@ async def remove_reaction( Parameters ---------- - emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] + emoji: Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] The emoji to remove. member: :class:`abc.Snowflake` The member for which to remove the reaction. @@ -1799,7 +1799,7 @@ async def clear_reaction(self, emoji: EmojiInputType | Reaction) -> None: Clears a specific reaction from the message. - The emoji may be a unicode emoji or a custom guild :class:`Emoji`. + The emoji may be a unicode emoji, a custom :class:`GuildEmoji`, or an :class:`AppEmoji`. You need the :attr:`~Permissions.manage_messages` permission to use this. @@ -1807,7 +1807,7 @@ async def clear_reaction(self, emoji: EmojiInputType | Reaction) -> None: Parameters ---------- - emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] + emoji: Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] The emoji to clear. Raises diff --git a/discord/onboarding.py b/discord/onboarding.py index bd98bbfdde..cf6a721a00 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: from .abc import Snowflake from .channel import ForumChannel, TextChannel, VoiceChannel - from .emoji import Emoji + from .emoji import GuildEmoji from .guild import Guild from .object import Object from .partial_emoji import PartialEmoji @@ -62,7 +62,7 @@ class PromptOption: The channels assigned to the user when they select this option. roles: List[:class:`Snowflake`] The roles assigned to the user when they select this option. - emoji: Union[:class:`Emoji`, :class:`PartialEmoji`] + emoji: Union[:class:`GuildEmoji`, :class:`PartialEmoji`] The emoji displayed with the option. title: :class:`str` The option's title. @@ -76,7 +76,7 @@ def __init__( channels: list[Snowflake] | None = None, roles: list[Snowflake] | None = None, description: str | None = None, - emoji: Emoji | PartialEmoji | None = None, + emoji: GuildEmoji | PartialEmoji | None = None, id: int | None = None, ): # ID is required when making edits, but it can be any snowflake that isn't already used by another prompt during edits @@ -85,7 +85,7 @@ def __init__( self.channels: list[Snowflake] = channels or [] self.roles: list[Snowflake] = roles or [] self.description: str | None = description or None - self.emoji: Emoji | PartialEmoji | None = emoji + self.emoji: GuildEmoji | PartialEmoji | None = emoji def __repr__(self): return f"" diff --git a/discord/poll.py b/discord/poll.py index 25c964cfec..087d2833f6 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: from .abc import Snowflake - from .emoji import Emoji + from .emoji import AppEmoji, GuildEmoji from .message import Message, PartialMessage from .types.poll import Poll as PollPayload from .types.poll import PollAnswer as PollAnswerPayload @@ -62,13 +62,15 @@ class PollMedia: text: :class:`str` The question/answer text. May have up to 300 characters for questions and 55 characters for answers. - emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + emoji: Optional[Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`PartialEmoji`, :class:`str`]] The answer's emoji. """ - def __init__(self, text: str, emoji: Emoji | PartialEmoji | str | None = None): + def __init__( + self, text: str, emoji: GuildEmoji | AppEmoji | PartialEmoji | str | None = None + ): self.text: str = text - self.emoji: Emoji | PartialEmoji | str | None = emoji + self.emoji: GuildEmoji | AppEmoji | PartialEmoji | str | None = emoji def to_dict(self) -> PollMediaPayload: dict_ = { @@ -124,7 +126,9 @@ class PollAnswer: The relevant media for this answer. """ - def __init__(self, text: str, emoji: Emoji | PartialEmoji | str | None = None): + def __init__( + self, text: str, emoji: GuildEmoji | AppEmoji | PartialEmoji | str | None = None + ): self.media = PollMedia(text, emoji) self.id = None self._poll = None @@ -135,7 +139,7 @@ def text(self) -> str: return self.media.text @property - def emoji(self) -> Emoji | PartialEmoji | None: + def emoji(self) -> GuildEmoji | AppEmoji | PartialEmoji | None: """The answer's emoji. Shortcut for :attr:`PollMedia.emoji`.""" return self.media.emoji @@ -446,7 +450,10 @@ def get_answer(self, id) -> PollAnswer | None: return utils.get(self.answers, id=id) def add_answer( - self, text: str, *, emoji: Emoji | PartialEmoji | str | None = None + self, + text: str, + *, + emoji: GuildEmoji | AppEmoji | PartialEmoji | str | None = None, ) -> Poll: """Add an answer to this poll. @@ -457,7 +464,7 @@ def add_answer( ---------- text: :class:`str` The answer text. Maximum 55 characters. - emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + emoji: Optional[Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`PartialEmoji`, :class:`str`]] The answer's emoji. Raises diff --git a/discord/reaction.py b/discord/reaction.py index 2726e8984f..8a2e000c8c 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -35,7 +35,7 @@ if TYPE_CHECKING: from .abc import Snowflake - from .emoji import Emoji + from .emoji import AppEmoji, GuildEmoji from .message import Message from .partial_emoji import PartialEmoji from .types.message import Reaction as ReactionPayload @@ -70,7 +70,7 @@ class Reaction: Attributes ---------- - emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`] + emoji: Union[:class:`GuildEmoji`, :class:`AppEmoji`, :class:`PartialEmoji`, :class:`str`] The reaction emoji. May be a custom emoji, or a unicode emoji. count: :class:`int` The combined total of normal and super reactions for this emoji. @@ -100,10 +100,10 @@ def __init__( *, message: Message, data: ReactionPayload, - emoji: PartialEmoji | Emoji | str | None = None, + emoji: PartialEmoji | GuildEmoji | AppEmoji | str | None = None, ): self.message: Message = message - self.emoji: PartialEmoji | Emoji | str = ( + self.emoji: PartialEmoji | GuildEmoji | AppEmoji | str = ( emoji or message._state.get_reaction_emoji(data["emoji"]) ) self.count: int = data.get("count", 1) diff --git a/discord/state.py b/discord/state.py index 4170d33fef..cf74d99285 100644 --- a/discord/state.py +++ b/discord/state.py @@ -49,7 +49,7 @@ from .automod import AutoModRule from .channel import * from .channel import _channel_factory -from .emoji import Emoji +from .emoji import AppEmoji, GuildEmoji from .enums import ChannelType, InteractionType, ScheduledEventStatus, Status, try_enum from .flags import ApplicationFlags, Intents, MemberCacheFlags from .guild import Guild @@ -251,6 +251,8 @@ def __init__( self.store_user = self.create_user # type: ignore self.deref_user = self.deref_user_no_intents # type: ignore + self.cache_app_emojis: bool = options.get("cache_app_emojis", False) + self.parsers = parsers = {} for attr, func in inspect.getmembers(self): if attr.startswith("parse_"): @@ -273,7 +275,7 @@ def clear(self, *, views: bool = True) -> None: # using __del__. Testing this for memory leaks led to no discernible leaks, # though more testing will have to be done. self._users: dict[int, User] = {} - self._emojis: dict[int, Emoji] = {} + self._emojis: dict[int, (GuildEmoji, AppEmoji)] = {} self._stickers: dict[int, GuildSticker] = {} self._guilds: dict[int, Guild] = {} self._polls: dict[int, Poll] = {} @@ -374,10 +376,20 @@ def get_user(self, id: int | None) -> User | None: # the keys of self._users are ints return self._users.get(id) # type: ignore - def store_emoji(self, guild: Guild, data: EmojiPayload) -> Emoji: + def store_emoji(self, guild: Guild, data: EmojiPayload) -> GuildEmoji: # the id will be present here emoji_id = int(data["id"]) # type: ignore - self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data) + self._emojis[emoji_id] = emoji = GuildEmoji(guild=guild, state=self, data=data) + return emoji + + def maybe_store_app_emoji( + self, application_id: int, data: EmojiPayload + ) -> AppEmoji: + # the id will be present here + emoji = AppEmoji(application_id=application_id, state=self, data=data) + if self.cache_app_emojis: + emoji_id = int(data["id"]) # type: ignore + self._emojis[emoji_id] = emoji return emoji def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker: @@ -413,7 +425,7 @@ def _remove_guild(self, guild: Guild) -> None: self._guilds.pop(guild.id, None) for emoji in guild.emojis: - self._emojis.pop(emoji.id, None) + self._remove_emoji(emoji) for sticker in guild.stickers: self._stickers.pop(sticker.id, None) @@ -421,17 +433,20 @@ def _remove_guild(self, guild: Guild) -> None: del guild @property - def emojis(self) -> list[Emoji]: + def emojis(self) -> list[GuildEmoji | AppEmoji]: return list(self._emojis.values()) @property def stickers(self) -> list[GuildSticker]: return list(self._stickers.values()) - def get_emoji(self, emoji_id: int | None) -> Emoji | None: + def get_emoji(self, emoji_id: int | None) -> GuildEmoji | AppEmoji | None: # the keys of self._emojis are ints return self._emojis.get(emoji_id) # type: ignore + def _remove_emoji(self, emoji: GuildEmoji | AppEmoji) -> None: + self._emojis.pop(emoji.id, None) + def get_sticker(self, sticker_id: int | None) -> GuildSticker | None: # the keys of self._stickers are ints return self._stickers.get(sticker_id) # type: ignore @@ -587,6 +602,11 @@ async def query_members( raise async def _delay_ready(self) -> None: + + if self.cache_app_emojis and self.application_id: + data = await self.http.get_all_application_emojis(self.application_id) + for e in data.get("items", []): + self.maybe_store_app_emoji(self.application_id, e) try: states = [] while True: @@ -1932,7 +1952,7 @@ def _get_reaction_user( return channel.guild.get_member(user_id) return self.get_user(user_id) - def get_reaction_emoji(self, data) -> Emoji | PartialEmoji: + def get_reaction_emoji(self, data) -> GuildEmoji | AppEmoji | PartialEmoji: emoji_id = utils._get_as_snowflake(data, "id") if not emoji_id: @@ -1948,7 +1968,9 @@ def get_reaction_emoji(self, data) -> Emoji | PartialEmoji: name=data["name"], ) - def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Emoji | PartialEmoji | str: + def _upgrade_partial_emoji( + self, emoji: PartialEmoji + ) -> GuildEmoji | AppEmoji | PartialEmoji | str: emoji_id = emoji.id if not emoji_id: return emoji.name @@ -2086,6 +2108,11 @@ async def _delay_ready(self) -> None: self.dispatch("shard_ready", shard_id) + if self.cache_app_emojis and self.application_id: + data = await self.http.get_all_application_emojis(self.application_id) + for e in data.get("items", []): + self.maybe_store_app_emoji(self.application_id, e) + # remove the state try: del self._ready_state diff --git a/discord/ui/button.py b/discord/ui/button.py index 5487fd6b73..42d4af8d08 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -40,7 +40,7 @@ ) if TYPE_CHECKING: - from ..emoji import Emoji + from ..emoji import AppEmoji, GuildEmoji from .view import View B = TypeVar("B", bound="Button") @@ -65,7 +65,7 @@ class Button(Item[V]): Whether the button is disabled or not. label: Optional[:class:`str`] The label of the button, if any. Maximum of 80 chars. - emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] + emoji: Optional[Union[:class:`.PartialEmoji`, :class:`GuildEmoji`, :class:`AppEmoji`, :class:`str`]] The emoji of the button, if available. sku_id: Optional[Union[:class:`int`]] The ID of the SKU this button refers to. @@ -95,7 +95,7 @@ def __init__( disabled: bool = False, custom_id: str | None = None, url: str | None = None, - emoji: str | Emoji | PartialEmoji | None = None, + emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, sku_id: int | None = None, row: int | None = None, ): @@ -132,7 +132,7 @@ def __init__( emoji = emoji._to_partial() else: raise TypeError( - "expected emoji to be str, Emoji, or PartialEmoji not" + "expected emoji to be str, GuildEmoji, AppEmoji, or PartialEmoji not" f" {emoji.__class__}" ) @@ -210,7 +210,7 @@ def emoji(self) -> PartialEmoji | None: return self._underlying.emoji @emoji.setter - def emoji(self, value: str | Emoji | PartialEmoji | None): # type: ignore + def emoji(self, value: str | GuildEmoji | AppEmoji | PartialEmoji | None): # type: ignore if value is None: self._underlying.emoji = None elif isinstance(value, str): @@ -219,7 +219,7 @@ def emoji(self, value: str | Emoji | PartialEmoji | None): # type: ignore self._underlying.emoji = value._to_partial() else: raise TypeError( - "expected str, Emoji, or PartialEmoji, received" + "expected str, GuildEmoji, AppEmoji, or PartialEmoji, received" f" {value.__class__} instead" ) @@ -275,7 +275,7 @@ def button( custom_id: str | None = None, disabled: bool = False, style: ButtonStyle = ButtonStyle.secondary, - emoji: str | Emoji | PartialEmoji | None = None, + emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, row: int | None = None, ) -> Callable[[ItemCallbackType], ItemCallbackType]: """A decorator that attaches a button to a component. @@ -302,9 +302,9 @@ def button( The style of the button. Defaults to :attr:`.ButtonStyle.grey`. disabled: :class:`bool` Whether the button is disabled or not. Defaults to ``False``. - emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] + emoji: Optional[Union[:class:`str`, :class:`GuildEmoji`, :class:`AppEmoji`, :class:`.PartialEmoji`]] The emoji of the button. This can be in string form or a :class:`.PartialEmoji` - or a full :class:`.Emoji`. + or a full :class:`GuildEmoji` or :class:`AppEmoji`. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 rows. By default, items are arranged automatically into those 5 rows. If you'd diff --git a/discord/ui/select.py b/discord/ui/select.py index fb55d39d1d..496446e61c 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -31,7 +31,7 @@ from ..channel import _threaded_guild_channel_factory from ..components import SelectMenu, SelectOption -from ..emoji import Emoji +from ..emoji import AppEmoji, GuildEmoji from ..enums import ChannelType, ComponentType from ..errors import InvalidArgument from ..interactions import Interaction @@ -260,7 +260,7 @@ def add_option( label: str, value: str = MISSING, description: str | None = None, - emoji: str | Emoji | PartialEmoji | None = None, + emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, default: bool = False, ): """Adds an option to the select menu. @@ -279,9 +279,9 @@ def add_option( description: Optional[:class:`str`] An additional description of the option, if any. Can only be up to 100 characters. - emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] + emoji: Optional[Union[:class:`str`, :class:`GuildEmoji`, :class:`AppEmoji`, :class:`.PartialEmoji`]] The emoji of the option, if available. This can either be a string representing - the custom or unicode emoji or an instance of :class:`.PartialEmoji` or :class:`.Emoji`. + the custom or unicode emoji or an instance of :class:`.PartialEmoji`, :class:`GuildEmoji`, or :class:`AppEmoji`. default: :class:`bool` Whether this option is selected by default. diff --git a/discord/welcome_screen.py b/discord/welcome_screen.py index 7e709ab7a6..cb0d1eea0a 100644 --- a/discord/welcome_screen.py +++ b/discord/welcome_screen.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: from .abc import Snowflake - from .emoji import Emoji + from .emoji import GuildEmoji from .guild import Guild from .partial_emoji import PartialEmoji from .types.welcome_screen import WelcomeScreen as WelcomeScreenPayload @@ -58,7 +58,7 @@ class WelcomeScreenChannel: The channel that is being referenced. description: :class:`str` The description of the channel that is shown on the welcome screen. - emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`] + emoji: Union[:class:`GuildEmoji`, :class:`PartialEmoji`, :class:`str`] The emoji of the channel that is shown on welcome screen. """ @@ -66,7 +66,7 @@ def __init__( self, channel: Snowflake, description: str, - emoji: Emoji | PartialEmoji | str, + emoji: GuildEmoji | PartialEmoji | str, ): self.channel = channel self.description = description diff --git a/docs/api/enums.rst b/docs/api/enums.rst index c734e2fd1e..cd48a85cf5 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -1190,7 +1190,7 @@ of :class:`enum.Enum`. An emoji was created. When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Emoji` or :class:`Object` with the emoji ID. + the :class:`GuildEmoji` or :class:`Object` with the emoji ID. Possible attributes for :class:`AuditLogDiff`: @@ -1201,7 +1201,7 @@ of :class:`enum.Enum`. An emoji was updated. This triggers when the name has changed. When this is the action, the type of :attr:`~AuditLogEntry.target` is - the :class:`Emoji` or :class:`Object` with the emoji ID. + the :class:`GuildEmoji` or :class:`Object` with the emoji ID. Possible attributes for :class:`AuditLogDiff`: diff --git a/docs/api/events.rst b/docs/api/events.rst index 1b011fa1f4..51652ad925 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -518,16 +518,16 @@ Guilds .. function:: on_guild_emojis_update(guild, before, after) - Called when a :class:`Guild` adds or removes an :class:`Emoji`. + Called when a :class:`Guild` adds or removes an :class:`GuildEmoji`. This requires :attr:`Intents.emojis_and_stickers` to be enabled. :param guild: The guild who got their emojis updated. :type guild: :class:`Guild` :param before: A list of emojis before the update. - :type before: Sequence[:class:`Emoji`] + :type before: Sequence[:class:`GuildEmoji`] :param after: A list of emojis after the update. - :type after: Sequence[:class:`Emoji`] + :type after: Sequence[:class:`GuildEmoji`] .. function:: on_guild_stickers_update(guild, before, after) diff --git a/docs/api/models.rst b/docs/api/models.rst index bea6b3c0de..0238e2fc77 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -388,9 +388,15 @@ Interactions Emoji ----- -.. attributetable:: Emoji +.. attributetable:: GuildEmoji -.. autoclass:: Emoji() +.. autoclass:: GuildEmoji() + :members: + :inherited-members: + +.. attributetable:: AppEmoji + +.. autoclass:: AppEmoji() :members: :inherited-members: diff --git a/docs/ext/commands/commands.rst b/docs/ext/commands/commands.rst index c907e2a1b4..686f95f047 100644 --- a/docs/ext/commands/commands.rst +++ b/docs/ext/commands/commands.rst @@ -394,7 +394,7 @@ A lot of discord models work out of the gate as a parameter: - :class:`Role` - :class:`Game` - :class:`Colour` -- :class:`Emoji` +- :class:`GuildEmoji` - :class:`PartialEmoji` - :class:`Thread` (since v2.0) @@ -437,7 +437,7 @@ converter is given below: +--------------------------+-------------------------------------------------+ | :class:`Colour` | :class:`~ext.commands.ColourConverter` | +--------------------------+-------------------------------------------------+ -| :class:`Emoji` | :class:`~ext.commands.EmojiConverter` | +| :class:`GuildEmoji` | :class:`~ext.commands.EmojiConverter` | +--------------------------+-------------------------------------------------+ | :class:`PartialEmoji` | :class:`~ext.commands.PartialEmojiConverter` | +--------------------------+-------------------------------------------------+ diff --git a/docs/faq.rst b/docs/faq.rst index dd6ee78e17..bf8bfe82d0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -199,7 +199,7 @@ Quick example: :: In case you want to use emoji that come from a message, you already get their code points in the content without needing to do anything special. You **cannot** send ``':thumbsup:'`` style shorthands. -For custom emoji, you should pass an instance of :class:`Emoji`. You can also pass a ``'<:name:id>'`` string, but if you +For custom emoji, you should pass an instance of :class:`GuildEmoji` or :class:`AppEmoji`. You can also pass a ``'<:name:id>'`` string, but if you can use said emoji, you should be able to use :meth:`Client.get_emoji` to get an emoji via ID or use :func:`utils.find`/ :func:`utils.get` on :attr:`Client.emojis` or :attr:`Guild.emojis` collections.