diff --git a/disnake/abc.py b/disnake/abc.py index c0a5211698..6d69fbf2c6 100644 --- a/disnake/abc.py +++ b/disnake/abc.py @@ -67,7 +67,7 @@ from .channel import CategoryChannel, DMChannel, PartialMessageable from .client import Client from .embeds import Embed - from .emoji import Emoji + from .emoji import AnyEmoji from .enums import InviteTarget from .guild import Guild, GuildMessageable from .guild_scheduled_event import GuildScheduledEvent @@ -338,7 +338,7 @@ async def _edit( video_quality_mode: VideoQualityMode = MISSING, flags: ChannelFlags = MISSING, available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, default_sort_order: Optional[ThreadSortOrder] = MISSING, default_layout: ThreadLayout = MISSING, reason: Optional[str] = None, diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py index c5ec6caf79..8d623d66ab 100644 --- a/disnake/audit_logs.py +++ b/disnake/audit_logs.py @@ -43,7 +43,7 @@ from .app_commands import APIApplicationCommand from .automod import AutoModRule - from .emoji import Emoji + from .emoji import AnyEmoji, Emoji from .guild import Guild from .guild_scheduled_event import GuildScheduledEvent from .integrations import PartialIntegration @@ -280,7 +280,7 @@ def _transform_automod_trigger_metadata( def _transform_default_reaction( entry: AuditLogEntry, data: Optional[DefaultReactionPayload] -) -> Optional[Union[Emoji, PartialEmoji]]: +) -> Optional[Union[AnyEmoji, PartialEmoji]]: if data is None: return None return entry._state._get_emoji_from_fields( @@ -817,7 +817,7 @@ def _convert_target_invite(self, target_id: int) -> Invite: def _convert_target_webhook(self, target_id: int) -> Union[Webhook, Object]: return self._webhooks.get(target_id) or Object(id=target_id) - def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]: + def _convert_target_emoji(self, target_id: int) -> Union[AnyEmoji, Object]: return self._state.get_emoji(target_id) or Object(id=target_id) def _convert_target_message(self, target_id: int) -> Union[Member, User, Object, None]: diff --git a/disnake/channel.py b/disnake/channel.py index 8eaf8dd794..716d69f229 100644 --- a/disnake/channel.py +++ b/disnake/channel.py @@ -69,7 +69,7 @@ from .abc import Snowflake, SnowflakeTime from .asset import AssetBytes from .embeds import Embed - from .emoji import Emoji + from .emoji import AnyEmoji from .guild import Guild, GuildChannel as GuildChannelType from .member import Member, VoiceState from .message import AllowedMentions, Message, PartialMessage @@ -3314,7 +3314,7 @@ def requires_tag(self) -> bool: return self.flags.require_tag @property - def default_reaction(self) -> Optional[Union[Emoji, PartialEmoji]]: + def default_reaction(self) -> Optional[Union[AnyEmoji, PartialEmoji]]: """Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]: The default emoji shown for reacting to threads. @@ -3932,7 +3932,7 @@ async def edit( flags: ChannelFlags = ..., require_tag: bool = ..., available_tags: Sequence[ForumTag] = ..., - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = ..., default_sort_order: Optional[ThreadSortOrder] = ..., default_layout: ThreadLayout = ..., reason: Optional[str] = ..., @@ -3955,7 +3955,7 @@ async def edit( flags: ChannelFlags = MISSING, require_tag: bool = MISSING, available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, default_sort_order: Optional[ThreadSortOrder] = MISSING, default_layout: ThreadLayout = MISSING, reason: Optional[str] = None, @@ -4102,7 +4102,7 @@ async def clone( default_thread_slowmode_delay: Optional[int] = MISSING, default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, default_sort_order: Optional[ThreadSortOrder] = MISSING, default_layout: ThreadLayout = MISSING, overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, @@ -4362,7 +4362,7 @@ async def edit( flags: ChannelFlags = ..., require_tag: bool = ..., available_tags: Sequence[ForumTag] = ..., - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = ..., default_sort_order: Optional[ThreadSortOrder] = ..., reason: Optional[str] = ..., ) -> MediaChannel: @@ -4384,7 +4384,7 @@ async def edit( flags: ChannelFlags = MISSING, require_tag: bool = MISSING, available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, default_sort_order: Optional[ThreadSortOrder] = MISSING, reason: Optional[str] = None, **kwargs: Never, @@ -4503,7 +4503,7 @@ async def clone( default_thread_slowmode_delay: Optional[int] = MISSING, default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, available_tags: Sequence[ForumTag] = MISSING, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, default_sort_order: Optional[ThreadSortOrder] = MISSING, overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, reason: Optional[str] = None, diff --git a/disnake/client.py b/disnake/client.py index 885a6979fe..dbbe26be52 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -46,7 +46,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 AnyEmoji, ApplicationEmoji from .entitlement import Entitlement from .enums import ApplicationCommandType, ChannelType, Event, Status from .errors import ( @@ -373,6 +373,9 @@ class Client: application commands. .. versionadded:: 2.5 + cache_app_emojis: :class:`bool` + Whether to automatically fetch and cache the application's emojis on startup and when fetching. + Defaults to ``False``. """ def __init__( @@ -401,6 +404,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, ) -> None: # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore @@ -444,6 +448,7 @@ def __init__( intents=intents, chunk_guilds_at_startup=chunk_guilds_at_startup, member_cache_flags=member_cache_flags, + cache_app_emojis=cache_app_emojis, ) self.shard_id: Optional[int] = shard_id self.shard_count: Optional[int] = shard_count @@ -492,6 +497,7 @@ def _get_state( application_id: Optional[int], heartbeat_timeout: float, guild_ready_timeout: float, + cache_app_emojis: bool, allowed_mentions: Optional[AllowedMentions], activity: Optional[BaseActivity], status: Optional[Union[str, Status]], @@ -515,6 +521,7 @@ def _get_state( intents=intents, chunk_guilds_at_startup=chunk_guilds_at_startup, member_cache_flags=member_cache_flags, + cache_app_emojis=cache_app_emojis, ) def _handle_ready(self) -> None: @@ -559,8 +566,13 @@ def guilds(self) -> List[Guild]: return self._connection.guilds @property - def emojis(self) -> List[Emoji]: - """List[:class:`.Emoji`]: The emojis that the connected client has.""" + def emojis(self) -> List[AnyEmoji]: + """The emojis that the connected client has. + + .. note:: + This only includes the application's emojis if :attr:`.cache_app_emojis` is + ``True``. + """ return self._connection.emojis @property @@ -1474,7 +1486,7 @@ def get_user(self, id: int, /) -> Optional[User]: """ return self._connection.get_user(id) - def get_emoji(self, id: int, /) -> Optional[Emoji]: + def get_emoji(self, id: int, /) -> Optional[AnyEmoji]: """Returns an emoji with the given ID. Parameters @@ -1484,7 +1496,7 @@ def get_emoji(self, id: int, /) -> Optional[Emoji]: Returns ------- - Optional[:class:`.Emoji`] + Optional[Union[:class:`disnake.emoji.GuildEmoji`, :class:`disnake.emoji.ApplicationEmoji`]] The custom emoji or ``None`` if not found. """ return self._connection.get_emoji(id) @@ -3211,3 +3223,81 @@ async def create_entitlement( owner_type=2 if isinstance(owner, abc.User) else 1, ) return Entitlement(data=data, state=self._connection) + + async def fetch_application_emojis(self) -> list[ApplicationEmoji]: + r"""|coro| + Retrieves all custom :class:`disnake.emoji.ApplicationEmoji`\s from the application. + + Raises + ------ + HTTPException + An error occurred fetching the emojis. + + Returns + ------- + List[:class:`disnake.emoji.ApplicationEmoji`] + The retrieved emojis. + """ + emojis = await self._connection.http.get_all_application_emojis(self.application_id) + return [ + self._connection.store_application_emoji(self.application_id, e) + for e in emojis["items"] + ] + + async def fetch_application_emoji(self, emoji_id: int, /) -> ApplicationEmoji: + """|coro| + Retrieves a custom :class:`disnake.emoji.ApplicationEmoji` from the application. + + Parameters + ---------- + emoji_id: :class:`int` + The emoji's ID. + + Returns + ------- + :class:`disnake.emoji.ApplicationEmoji` + 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.store_application_emoji(self.application_id, data) + + async def create_application_emoji( + self, + *, + name: str, + image: bytes, + ) -> ApplicationEmoji: + r"""|coro| + Creates a custom :class:`disnake.emoji.ApplicationEmoji` 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 + ------ + Forbidden + You are not allowed to create emojis. + HTTPException + An error occurred creating an emoji. + + Returns + ------- + :class:`disnake.emoji.ApplicationEmoji` + 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.store_application_emoji(self.application_id, data) diff --git a/disnake/components.py b/disnake/components.py index 7614fd424b..ba647c2d50 100644 --- a/disnake/components.py +++ b/disnake/components.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from typing_extensions import Self, TypeAlias - from .emoji import Emoji + from .emoji import AnyEmoji from .types.components import ( ActionRow as ActionRowPayload, AnySelectMenu as AnySelectMenuPayload, @@ -546,7 +546,7 @@ def __init__( label: str, value: str = MISSING, description: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, default: bool = False, ) -> None: self.label = label diff --git a/disnake/emoji.py b/disnake/emoji.py index badedbce86..9dc4a2255a 100644 --- a/disnake/emoji.py +++ b/disnake/emoji.py @@ -9,7 +9,7 @@ from .user import User from .utils import MISSING, SnowflakeList, snowflake_time -__all__ = ("Emoji",) +__all__ = ("BaseEmoji", "Emoji", "GuildEmoji", "ApplicationEmoji", "AnyEmoji") if TYPE_CHECKING: from datetime import datetime @@ -22,11 +22,15 @@ from .types.emoji import Emoji as EmojiPayload -class Emoji(_EmojiTag, AssetMixin): - """Represents a custom emoji. +class BaseEmoji(_EmojiTag, AssetMixin): + """Represents an abstract custom emoji. - Depending on the way this object was created, some of the attributes can - have a value of ``None``. + This is usually represented as a custom emoji. + + This isn't meant to be used directly, instead use one of the concrete custom emoji types: + + - :class:`GuildEmoji` + - :class:`ApplicationEmoji` .. collapse:: operations @@ -63,8 +67,6 @@ class Emoji(_EmojiTag, AssetMixin): Whether the emoji is animated or not. managed: :class:`bool` Whether the emoji is managed by a Twitch integration. - guild_id: :class:`int` - The guild ID the emoji belongs to. available: :class:`bool` Whether the emoji is available for use. user: Optional[:class:`User`] @@ -80,15 +82,11 @@ class Emoji(_EmojiTag, AssetMixin): "id", "name", "_roles", - "guild_id", "user", "available", ) - def __init__( - self, *, guild: Union[Guild, GuildPreview], state: ConnectionState, data: EmojiPayload - ) -> None: - self.guild_id: int = guild.id + def __init__(self, *, state: ConnectionState, data: EmojiPayload) -> None: self._state: ConnectionState = state self._from_data(data) @@ -119,7 +117,7 @@ def __str__(self) -> str: return f"<:{self.name}:{self.id}>" def __repr__(self) -> str: - return f"" + return f"" def __eq__(self, other: Any) -> bool: return isinstance(other, _EmojiTag) and self.id == other.id @@ -141,6 +139,71 @@ def url(self) -> str: 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 of the attributes can + have a value of ``None``. + + This is an alias for :class:`Emoji`. + + .. collapse:: 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 emoji's name. + id: :class:`int` + The emoji's ID. + require_colons: :class:`bool` + Whether colons are required to use this emoji in the client (:PJSalt: vs PJSalt). + animated: :class:`bool` + Whether the emoji is animated or not. + managed: :class:`bool` + Whether the emoji is managed by a Twitch integration. + guild_id: :class:`int` + The guild ID the emoji belongs to. + available: :class:`bool` + Whether the emoji is available for use. + user: Optional[:class:`User`] + The user that created this emoji. This can only be retrieved using + :meth:`Guild.fetch_emoji`/:meth:`Guild.fetch_emojis` while + having the :attr:`~Permissions.manage_guild_expressions` permission. + """ + + __slots__: Tuple[str, ...] = ("guild_id",) + + def __init__( + self, *, guild: Union[Guild, GuildPreview], state: ConnectionState, data: EmojiPayload + ) -> None: + self.guild_id: int = guild.id + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return f"" + @property def roles(self) -> List[Role]: """List[:class:`Role`]: A :class:`list` of roles that are allowed to use this emoji. @@ -200,7 +263,7 @@ async def delete(self, *, reason: Optional[str] = None) -> None: async def edit( self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None - ) -> Emoji: + ) -> GuildEmoji: """|coro| Edits the custom emoji. @@ -233,7 +296,7 @@ async def edit( Returns ------- - :class:`Emoji` + :class:`GuildEmoji` The newly updated emoji. """ payload = {} @@ -245,4 +308,124 @@ 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 ApplicationEmoji(BaseEmoji): + """Represents a custom emoji from an application. + + Depending on the way this object was created, some attributes can + have a value of ``None``. + + .. 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) -> None: + self.application_id: int = application_id + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return f"" + + 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, + ) -> ApplicationEmoji: + 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:`ApplicationEmoji` + 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.store_application_emoji(self.application_id, data) + + +AnyEmoji = Union[GuildEmoji, ApplicationEmoji] diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 825f96e6ae..9d9c092196 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -215,6 +215,10 @@ class Bot(BotBase, InteractionBotBase, disnake.Client): application commands. .. versionadded:: 2.5 + + cache_app_emojis: :class:`bool` + Whether to automatically fetch and cache the application's emojis on startup and when fetching. + Defaults to ``False``. """ if TYPE_CHECKING: @@ -258,6 +262,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, ) -> None: @@ -310,6 +315,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, ) -> None: @@ -423,6 +429,10 @@ class InteractionBot(InteractionBotBase, disnake.Client): application commands. .. versionadded:: 2.5 + + cache_app_emojis: :class:`bool` + Whether to automatically fetch and cache the application's emojis on startup and when fetching. + Defaults to ``False``. """ if TYPE_CHECKING: @@ -459,6 +469,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, ) -> None: @@ -504,6 +515,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, localization_provider: Optional[LocalizationProtocol] = None, strict_localization: bool = False, ) -> None: diff --git a/disnake/guild.py b/disnake/guild.py index 301d926876..eb6b0b16eb 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -40,7 +40,7 @@ _threaded_guild_channel_factory, ) from .colour import Colour -from .emoji import Emoji +from .emoji import AnyEmoji, Emoji from .enums import ( AuditLogAction, AutoModEventType, @@ -1643,7 +1643,7 @@ async def create_forum_channel( nsfw: bool = MISSING, overwrites: Dict[Union[Role, Member], PermissionOverwrite] = MISSING, available_tags: Optional[Sequence[ForumTag]] = None, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = None, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, default_sort_order: Optional[ThreadSortOrder] = None, default_layout: Optional[ThreadLayout] = None, reason: Optional[str] = None, @@ -1793,7 +1793,7 @@ async def create_media_channel( nsfw: bool = MISSING, overwrites: Dict[Union[Role, Member], PermissionOverwrite] = MISSING, available_tags: Optional[Sequence[ForumTag]] = None, - default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = None, + default_reaction: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, default_sort_order: Optional[ThreadSortOrder] = None, reason: Optional[str] = None, ) -> MediaChannel: diff --git a/disnake/http.py b/disnake/http.py index f10cd3fdd8..985d98fd4e 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -1709,6 +1709,63 @@ def edit_custom_emoji( ) return self.request(r, json=payload, reason=reason) + def get_all_application_emojis( + self, application_id: Snowflake + ) -> Response[Dict[str, 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: str, + ) -> 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]]: r = Route("GET", "/guilds/{guild_id}/integrations", guild_id=guild_id) diff --git a/disnake/message.py b/disnake/message.py index 7957799de6..a588ff3f5a 100644 --- a/disnake/message.py +++ b/disnake/message.py @@ -25,7 +25,7 @@ from . import utils from .components import ActionRow, MessageComponent, _component_factory from .embeds import Embed -from .emoji import Emoji +from .emoji import AnyEmoji, BaseEmoji from .enums import ChannelType, InteractionType, MessageType, try_enum, try_enum_to_int from .errors import HTTPException from .file import File @@ -76,7 +76,7 @@ from .ui.action_row import Components, MessageUIComponent from .ui.view import View - EmojiInputType = Union[Emoji, PartialEmoji, str] + EmojiInputType = Union[AnyEmoji, PartialEmoji, str] __all__ = ( "Attachment", @@ -93,7 +93,7 @@ def convert_emoji_reaction(emoji: Union[EmojiInputType, Reaction]) -> str: if isinstance(emoji, Reaction): emoji = emoji.emoji - if isinstance(emoji, Emoji): + if isinstance(emoji, BaseEmoji): return f"{emoji.name}:{emoji.id}" if isinstance(emoji, PartialEmoji): return emoji._as_reaction() diff --git a/disnake/onboarding.py b/disnake/onboarding.py index 069e0fb7fa..e081f256a4 100644 --- a/disnake/onboarding.py +++ b/disnake/onboarding.py @@ -7,7 +7,7 @@ from .mixins import Hashable if TYPE_CHECKING: - from .emoji import Emoji, PartialEmoji + from .emoji import AnyEmoji, PartialEmoji from .guild import Guild, GuildChannel from .role import Role from .types.onboarding import ( @@ -170,7 +170,7 @@ def __init__(self, *, guild: Guild, data: OnboardingPromptOptionPayload) -> None else frozenset() ) - self.emoji: Optional[Union[Emoji, PartialEmoji, str]] + self.emoji: Optional[Union[AnyEmoji, PartialEmoji, str]] if emoji_data := data.get("emoji"): self.emoji = guild._state.get_reaction_emoji(emoji_data) else: diff --git a/disnake/partial_emoji.py b/disnake/partial_emoji.py index 92656bb314..59a9fa8f25 100644 --- a/disnake/partial_emoji.py +++ b/disnake/partial_emoji.py @@ -15,7 +15,7 @@ from typing_extensions import Self - from .emoji import Emoji + from .emoji import AnyEmoji from .state import ConnectionState from .types.activity import ActivityEmoji as ActivityEmojiPayload from .types.emoji import Emoji as EmojiPayload, PartialEmoji as PartialEmojiPayload @@ -254,7 +254,7 @@ async def read(self) -> bytes: # (e.g. default reaction, tag emoji) @staticmethod def _emoji_to_name_id( - emoji: Optional[Union[str, Emoji, PartialEmoji]] + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] ) -> Tuple[Optional[str], Optional[int]]: if emoji is None: return None, None diff --git a/disnake/reaction.py b/disnake/reaction.py index 0720759f6a..6c3925e8a3 100644 --- a/disnake/reaction.py +++ b/disnake/reaction.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from .abc import Snowflake - from .emoji import Emoji + from .emoji import AnyEmoji, Emoji from .message import Message from .partial_emoji import PartialEmoji from .types.message import Reaction as ReactionPayload @@ -61,7 +61,7 @@ def __init__( *, message: Message, data: ReactionPayload, - emoji: Optional[Union[PartialEmoji, Emoji, str]] = None, + emoji: Optional[Union[PartialEmoji, AnyEmoji, str]] = None, ) -> None: self.message: Message = message # _get_emoji_from_data won't return None diff --git a/disnake/state.py b/disnake/state.py index f4885513d7..3cb979c0c6 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -44,7 +44,7 @@ VoiceChannel, _guild_channel_factory, ) -from .emoji import Emoji +from .emoji import AnyEmoji, ApplicationEmoji, GuildEmoji from .entitlement import Entitlement from .enums import ApplicationCommandType, ChannelType, ComponentType, MessageType, Status, try_enum from .flags import ApplicationFlags, Intents, MemberCacheFlags @@ -204,6 +204,7 @@ def __init__( intents: Optional[Intents] = None, chunk_guilds_at_startup: Optional[bool] = None, member_cache_flags: Optional[MemberCacheFlags] = None, + cache_app_emojis: bool = False, ) -> None: self.loop: asyncio.AbstractEventLoop = loop self.http: HTTPClient = http @@ -277,6 +278,8 @@ def __init__( if not self._intents.members or member_cache_flags._empty: self.store_user = self.create_user + self.cache_app_emojis: bool = cache_app_emojis + self.parsers = parsers = {} for attr, func in inspect.getmembers(self): if attr.startswith("parse_"): @@ -293,7 +296,7 @@ def clear( # - the weakref slot + object in user objects likely results in a small increase in memory usage # - accesses on `_users` are slower, e.g. `__getitem__` takes ~1us with weakrefs and ~0.2us without self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary() - self._emojis: Dict[int, Emoji] = {} + self._emojis: Dict[int, AnyEmoji] = {} self._stickers: Dict[int, GuildSticker] = {} self._guilds: Dict[int, Guild] = {} @@ -394,10 +397,17 @@ def get_user(self, id: Optional[int]) -> Optional[User]: # 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 store_application_emoji(self, application_id: int, data: EmojiPayload) -> ApplicationEmoji: + emoji = ApplicationEmoji(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: @@ -435,7 +445,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) @@ -508,17 +518,20 @@ def _get_guild_command_named( return cmd @property - def emojis(self) -> List[Emoji]: + def emojis(self) -> List[AnyEmoji]: return list(self._emojis.values()) @property def stickers(self) -> List[GuildSticker]: return list(self._stickers.values()) - def get_emoji(self, emoji_id: Optional[int]) -> Optional[Emoji]: + def get_emoji(self, emoji_id: Optional[int]) -> Optional[AnyEmoji]: # the keys of self._emojis are ints return self._emojis.get(emoji_id) # type: ignore + def _remove_emoji(self, emoji: AnyEmoji) -> None: + self._emojis.pop(emoji.id, None) + def get_sticker(self, sticker_id: Optional[int]) -> Optional[GuildSticker]: # the keys of self._stickers are ints return self._stickers.get(sticker_id) # type: ignore @@ -1929,7 +1942,7 @@ def _get_reaction_user( def _get_emoji_from_data( self, data: PartialEmojiPayload - ) -> Optional[Union[str, Emoji, PartialEmoji]]: + ) -> Optional[Union[str, AnyEmoji, PartialEmoji]]: """Convert partial emoji data to proper emoji. Returns unicode emojis as strings. @@ -1960,7 +1973,7 @@ def _get_emoji_from_fields( name: Optional[str], id: Optional[int], animated: Optional[bool] = False, - ) -> Optional[Union[Emoji, PartialEmoji]]: + ) -> Optional[Union[AnyEmoji, PartialEmoji]]: """Convert partial emoji fields to proper emoji, if possible. If both `id` and `name` are nullish, returns `None`. @@ -1986,7 +1999,7 @@ def _get_emoji_from_fields( animated=animated or False, ) - def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Union[Emoji, PartialEmoji, str]: + def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Union[AnyEmoji, PartialEmoji, str]: emoji_id = emoji.id if not emoji_id: return emoji.name @@ -2273,6 +2286,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.store_application_emoji(self.application_id, e) + # remove the state try: del self._ready_state diff --git a/disnake/threads.py b/disnake/threads.py index 8095bbc9a3..ae609a6790 100644 --- a/disnake/threads.py +++ b/disnake/threads.py @@ -28,7 +28,7 @@ from .abc import Snowflake, SnowflakeTime from .channel import CategoryChannel, ForumChannel, MediaChannel, TextChannel - from .emoji import Emoji + from .emoji import AnyEmoji from .guild import Guild from .member import Member from .message import Message, PartialMessage @@ -1157,14 +1157,14 @@ def __init__( self, *, name: str, - emoji: Optional[Union[str, PartialEmoji, Emoji]] = None, + emoji: Optional[Union[str, PartialEmoji, AnyEmoji]] = None, moderated: bool = False, ) -> None: self.id: int = 0 self.name: str = name self.moderated: bool = moderated - self.emoji: Optional[Union[Emoji, PartialEmoji]] = None + self.emoji: Optional[Union[AnyEmoji, PartialEmoji]] = None if emoji is None: self.emoji = None elif isinstance(emoji, str): @@ -1225,7 +1225,7 @@ def with_changes( self, *, name: str = MISSING, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = MISSING, moderated: bool = MISSING, ) -> Self: """Returns a new instance with the given changes applied, diff --git a/disnake/types/emoji.py b/disnake/types/emoji.py index 5b8bdcf756..c5e085a233 100644 --- a/disnake/types/emoji.py +++ b/disnake/types/emoji.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MIT -from typing import Optional, TypedDict +from typing import List, Optional, TypedDict from .snowflake import Snowflake, SnowflakeList from .user import User @@ -20,6 +20,10 @@ class Emoji(PartialEmoji, total=False): available: bool +class ApplicationEmojiList(TypedDict): + items: List[Emoji] + + class EditEmoji(TypedDict): name: str roles: Optional[SnowflakeList] diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py index 21ea01cb74..ef9337b582 100644 --- a/disnake/ui/action_row.py +++ b/disnake/ui/action_row.py @@ -40,7 +40,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ..emoji import Emoji + from ..emoji import AnyEmoji from ..message import Message from ..partial_emoji import PartialEmoji from ..types.components import ActionRow as ActionRowPayload @@ -245,7 +245,7 @@ def add_button( disabled: bool = False, custom_id: Optional[str] = None, url: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, ) -> ButtonCompatibleActionRowT: """Add a button to the action row. Can only be used if the action row holds message components. @@ -275,7 +275,7 @@ def add_button( Whether the button is disabled or not. label: Optional[:class:`str`] The label of the button, if any. - emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] + emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.GuildEmoji`, :class:`.ApplicationEmoji`, :class:`str`]] The emoji of the button, if available. Raises diff --git a/disnake/ui/button.py b/disnake/ui/button.py index a961ba29ab..e22d2b0206 100644 --- a/disnake/ui/button.py +++ b/disnake/ui/button.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from typing_extensions import ParamSpec, Self - from ..emoji import Emoji + from ..emoji import AnyEmoji from .item import ItemCallbackType from .view import View @@ -62,7 +62,7 @@ class Button(Item[V_co]): Whether the button is disabled. label: Optional[:class:`str`] The label of the button, if any. - emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]] + emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.GuildEmoji`, :class:`.ApplicationEmoji`, :class:`str`]] The emoji of the button, if available. row: Optional[:class:`int`] The relative row this button belongs to. A Discord component can only have 5 @@ -92,7 +92,7 @@ def __init__( disabled: bool = False, custom_id: Optional[str] = None, url: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, row: Optional[int] = None, ) -> None: ... @@ -106,7 +106,7 @@ def __init__( disabled: bool = False, custom_id: Optional[str] = None, url: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, row: Optional[int] = None, ) -> None: ... @@ -119,7 +119,7 @@ def __init__( disabled: bool = False, custom_id: Optional[str] = None, url: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, row: Optional[int] = None, ) -> None: super().__init__() @@ -217,7 +217,7 @@ def emoji(self) -> Optional[PartialEmoji]: return self._underlying.emoji @emoji.setter - def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None: + def emoji(self, value: Optional[Union[str, AnyEmoji, PartialEmoji]]) -> None: if value is not None: if isinstance(value, str): self._underlying.emoji = PartialEmoji.from_str(value) @@ -261,7 +261,7 @@ def button( custom_id: Optional[str] = None, disabled: bool = False, style: ButtonStyle = ButtonStyle.secondary, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, row: Optional[int] = None, ) -> Callable[[ItemCallbackType[Button[V_co]]], DecoratedItem[Button[V_co]]]: ... @@ -309,9 +309,9 @@ def button( The style of the button. Defaults to :attr:`.ButtonStyle.grey`. disabled: :class:`bool` Whether the button is disabled. Defaults to ``False``. - emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]] + emoji: Optional[Union[:class:`str`, :class:`.GuildEmoji`, :class:`.ApplicationEmoji`, :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`, :class:`.ApplicationEmoji`,. 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/disnake/ui/select/string.py b/disnake/ui/select/string.py index 3eeedc1f22..07198b685b 100644 --- a/disnake/ui/select/string.py +++ b/disnake/ui/select/string.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ...emoji import Emoji + from ...emoji import AnyEmoji from ...partial_emoji import PartialEmoji from ..item import DecoratedItem, ItemCallbackType, Object @@ -181,7 +181,7 @@ def add_option( label: str, value: str = MISSING, description: Optional[str] = None, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, default: bool = False, ) -> None: """Adds an option to the select menu. @@ -200,9 +200,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:`.ApplicationEmoji`, :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` or :class:`.GuildEmoji`, :class:`.ApplicationEmoji`. default: :class:`bool` Whether this option is selected by default. diff --git a/disnake/welcome_screen.py b/disnake/welcome_screen.py index a2e2fa171c..28f9058e0f 100644 --- a/disnake/welcome_screen.py +++ b/disnake/welcome_screen.py @@ -8,7 +8,7 @@ from .partial_emoji import PartialEmoji, _EmojiTag if TYPE_CHECKING: - from .emoji import Emoji + from .emoji import AnyEmoji from .guild import Guild from .invite import PartialInviteGuild from .state import ConnectionState @@ -51,11 +51,11 @@ def __init__( *, id: int, description: str, - emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + emoji: Optional[Union[str, AnyEmoji, PartialEmoji]] = None, ) -> None: self.id: int = id self.description: str = description - self.emoji: Optional[Union[Emoji, PartialEmoji]] = None + self.emoji: Optional[Union[AnyEmoji, PartialEmoji]] = None if emoji is None: self.emoji = None elif isinstance(emoji, str): diff --git a/docs/api/emoji.rst b/docs/api/emoji.rst index 3b4756d85b..5a89646453 100644 --- a/docs/api/emoji.rst +++ b/docs/api/emoji.rst @@ -2,8 +2,8 @@ .. currentmodule:: disnake -Emoji -===== +Emojis +====== This section documents everything related to Discord :ddocs:`emoji `. @@ -14,23 +14,48 @@ Discord Models Emoji ~~~~~ -.. attributetable:: Emoji +.. autoclass:: Emoji + :members: + :inherited-members: + +BaseEmoji +~~~~~~~~~ + +.. autoclass:: disnake.BaseEmoji + :members: + :inherited-members: + +.. attributetable:: BaseEmoji + +GuildEmoji +~~~~~~~~~~ + +.. autoclass:: disnake.GuildEmoji + :members: + :inherited-members: -.. autoclass:: Emoji() - :members: - :inherited-members: +.. attributetable:: GuildEmoji + +ApplicationEmoji +~~~~~~~~~~~~~~~~~ + +.. autoclass:: disnake.ApplicationEmoji + :members: + :inherited-members: + +.. attributetable:: ApplicationEmoji Data Classes -------------- +------------ PartialEmoji ~~~~~~~~~~~~ -.. attributetable:: PartialEmoji +.. autoclass:: disnake.PartialEmoji + :members: + :inherited-members: -.. autoclass:: PartialEmoji - :members: - :inherited-members: +.. attributetable:: PartialEmoji Events ------ diff --git a/docs/conf.py b/docs/conf.py index 5944191079..782004e475 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -78,7 +78,7 @@ .. |maybecoro| replace:: This function *could be a* |coroutine_link|_. .. |coroutine_link| replace:: *coroutine* .. |components_type| replace:: Union[:class:`disnake.ui.ActionRow`, :class:`disnake.ui.WrappedComponent`, List[Union[:class:`disnake.ui.ActionRow`, :class:`disnake.ui.WrappedComponent`, List[:class:`disnake.ui.WrappedComponent`]]]] -.. |resource_type| replace:: Union[:class:`bytes`, :class:`.Asset`, :class:`.Emoji`, :class:`.PartialEmoji`, :class:`.StickerItem`, :class:`.Sticker`] +.. |resource_type| replace:: Union[:class:`bytes`, :class:`.Asset`, :class:`disnake.emoji.ApplicationEmoji`, :class:`disnake.emoji.GuildEmoji`, :class:`.StickerItem`, :class:`.Sticker`] .. _coroutine_link: https://docs.python.org/3/library/asyncio-task.html#coroutine """ diff --git a/docs/locale/ja/LC_MESSAGES/api.po b/docs/locale/ja/LC_MESSAGES/api.po index 44f7348e9a..baa488be13 100644 --- a/docs/locale/ja/LC_MESSAGES/api.po +++ b/docs/locale/ja/LC_MESSAGES/api.po @@ -666,11 +666,11 @@ msgstr ":class:`.Guild`" #: disnake.Client.emojis:1 of #, fuzzy msgid "The emojis that the connected client has." -msgstr "List[:class:`.Emoji`] -- 接続したクライアントがアクセスできる絵文字。" +msgstr "List[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]] -- 接続したクライアントがアクセスできる絵文字。" #: disnake.Client.emojis:3 of #, fuzzy -msgid "List[:class:`.Emoji`]" +msgid "List[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]" msgstr ":class:`.Webhook`" #: disnake.Client.cached_messages:1 of @@ -1347,14 +1347,14 @@ msgstr ":class:`~disnake.User`" #: disnake.Client.get_emoji:1 of #, fuzzy msgid "Returns an emoji with the given ID." -msgstr "Optional[:class:`.Emoji`]: 与えられたIDを持つ絵文字を返します。" +msgstr "Optional[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]: 与えられたIDを持つ絵文字を返します。" #: disnake.Client.get_emoji:6 of msgid "The custom emoji or ``None`` if not found." msgstr "" #: disnake.Client.get_emoji:7 of -msgid "Optional[:class:`.Emoji`]" +msgid "Optional[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]" msgstr "" #: disnake.Client.get_all_channels:1 of diff --git a/docs/locale/ja/LC_MESSAGES/ext/commands/api.po b/docs/locale/ja/LC_MESSAGES/ext/commands/api.po index f822bbb6f6..b2ee08ff3a 100644 --- a/docs/locale/ja/LC_MESSAGES/ext/commands/api.po +++ b/docs/locale/ja/LC_MESSAGES/ext/commands/api.po @@ -1063,11 +1063,11 @@ msgstr ":exc:`.HTTPException` -- 招待の取り消しに失敗した。" #: disnake.ext.commands.Bot.emojis:1 of #, fuzzy msgid "The emojis that the connected client has." -msgstr "List[:class:`.Emoji`] -- 接続したクライアントがアクセスできる絵文字。" +msgstr "List[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]] -- 接続したクライアントがアクセスできる絵文字。" #: disnake.ext.commands.Bot.emojis:3 of #, fuzzy -msgid "List[:class:`.Emoji`]" +msgid "List[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]" msgstr ":class:`.Guild`" #: disnake.ext.commands.Bot.event:1 of @@ -1598,7 +1598,7 @@ msgid "The custom emoji or ``None`` if not found." msgstr "" #: disnake.ext.commands.Bot.get_emoji:7 of -msgid "Optional[:class:`.Emoji`]" +msgid "Optional[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]" msgstr "" #: disnake.ext.commands.Bot.get_guild:1 of @@ -6021,7 +6021,7 @@ msgstr "" #~ "from the internal list of commands." #~ msgstr "" -#~ msgid "Optional[:class:`.Emoji`]: Returns an emoji with the given ID." +#~ msgid "Optional[Union[:class:`GuildEmoji`, :class:`ApplicationEmoji`]]: Returns an emoji with the given ID." #~ msgstr "" #~ msgid "Optional[:class:`.Guild`]: Returns a guild with the given ID."