diff --git a/interactions/api/http/http_requests/emojis.py b/interactions/api/http/http_requests/emojis.py index c8a683cd0..1e94616c7 100644 --- a/interactions/api/http/http_requests/emojis.py +++ b/interactions/api/http/http_requests/emojis.py @@ -110,3 +110,88 @@ async def delete_guild_emoji( Route("DELETE", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id), reason=reason, ) + + async def get_application_emojis(self, application_id: "Snowflake_Type") -> list[discord_typings.EmojiData]: + """ + Fetch all emojis for this application + + Args: + application_id: The id of the application + + Returns: + List of emojis + + """ + result = await self.request(Route("GET", f"/applications/{application_id}/emojis")) + result = cast(dict, result) + return cast(list[discord_typings.EmojiData], result["items"]) + + async def get_application_emoji( + self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type" + ) -> discord_typings.EmojiData: + """ + Fetch an emoji for this application + + Args: + application_id: The id of the application + emoji_id: The id of the emoji + + Returns: + Emoji object + + """ + result = await self.request(Route("GET", f"/applications/{application_id}/emojis/{emoji_id}")) + return cast(discord_typings.EmojiData, result) + + async def create_application_emoji( + self, payload: dict, application_id: "Snowflake_Type", reason: str | None = None + ) -> discord_typings.EmojiData: + """ + Create an emoji for this application + + Args: + application_id: The id of the application + name: The name of the emoji + imagefile: The image file to use for the emoji + + Returns: + Emoji object + + """ + result = await self.request( + Route("POST", f"/applications/{application_id}/emojis"), payload=payload, reason=reason + ) + return cast(discord_typings.EmojiData, result) + + async def edit_application_emoji( + self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type", name: str + ) -> discord_typings.EmojiData: + """ + Edit an emoji for this application + + Args: + application_id: The id of the application + emoji_id: The id of the emoji + name: The new name for the emoji + + Returns: + Emoji object + + """ + result = await self.request( + Route("PATCH", f"/applications/{application_id}/emojis/{emoji_id}"), payload={"name": name} + ) + return cast(discord_typings.EmojiData, result) + + async def delete_application_emoji( + self, application_id: discord_typings.Snowflake, emoji_id: discord_typings.Snowflake + ) -> None: + """ + Delete an emoji for this application + + Args: + application_id: The id of the application + emoji_id: The id of the emoji + + """ + await self.request(Route("DELETE", f"/applications/{application_id}/emojis/{emoji_id}")) diff --git a/interactions/client/smart_cache.py b/interactions/client/smart_cache.py index bc7ab5b14..c7090a616 100644 --- a/interactions/client/smart_cache.py +++ b/interactions/client/smart_cache.py @@ -642,7 +642,7 @@ def place_guild_data(self, data: discord_typings.GuildData) -> Guild: """ guild_id = to_snowflake(data["id"]) - guild: Guild = self.guild_cache.get(guild_id) + guild: Guild | None = self.guild_cache.get(guild_id) if guild is None: guild = Guild.from_dict(data, self._client) self.guild_cache[guild_id] = guild @@ -914,7 +914,7 @@ def get_emoji(self, emoji_id: Optional["Snowflake_Type"]) -> Optional["CustomEmo """ return self.emoji_cache.get(to_optional_snowflake(emoji_id)) if self.emoji_cache is not None else None - def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.EmojiData) -> "CustomEmoji": + def place_emoji_data(self, guild_id: "Snowflake_Type | None", data: discord_typings.EmojiData) -> "CustomEmoji": """ Take json data representing an emoji, process it, and cache it. This cache is disabled by default, start your bot with `Client(enable_emoji_cache=True)` to enable it. @@ -929,7 +929,7 @@ def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.Emo with suppress(KeyError): del data["guild_id"] # discord sometimes packages a guild_id - this will cause an exception - emoji = CustomEmoji.from_dict(data, self._client, to_snowflake(guild_id)) + emoji = CustomEmoji.from_dict(data, self._client, to_optional_snowflake(guild_id)) if self.emoji_cache is not None: self.emoji_cache[emoji.id] = emoji diff --git a/interactions/models/discord/application.py b/interactions/models/discord/application.py index f6fdee89d..b35949754 100644 --- a/interactions/models/discord/application.py +++ b/interactions/models/discord/application.py @@ -4,8 +4,11 @@ from interactions.client.const import MISSING from interactions.client.utils.attr_converters import optional +from interactions.client.utils.serializer import to_image_data from interactions.models.discord.asset import Asset +from interactions.models.discord.emoji import CustomEmoji from interactions.models.discord.enums import ApplicationFlags +from interactions.models.discord.file import UPLOADABLE_TYPE from interactions.models.discord.snowflake import Snowflake_Type, to_snowflake from interactions.models.discord.team import Team from .base import DiscordObject @@ -88,3 +91,35 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any] def owner(self) -> "User": """The user object for the owner of this application""" return self._client.cache.get_user(self.owner_id) + + async def fetch_all_emoji(self) -> List[CustomEmoji]: + """Fetch all emojis for this application""" + response = await self.client.http.get_application_emojis(self.id) + return [self.client.cache.place_emoji_data(None, emoji) for emoji in response] + + async def fetch_emoji(self, emoji_id: Snowflake_Type) -> CustomEmoji: + """Fetch an emoji for this application""" + response = await self.client.http.get_application_emoji(self.id, emoji_id) + return self.client.cache.place_emoji_data(None, response) + + async def create_emoji(self, name: str, imagefile: UPLOADABLE_TYPE) -> CustomEmoji: + """Create an emoji for this application""" + data_payload = { + "name": name, + "image": to_image_data(imagefile), + "roles": MISSING, + } + + return self.client.cache.place_emoji_data( + None, await self.client.http.create_application_emoji(data_payload, self.id) + ) + + async def edit_emoji(self, emoji_id: Snowflake_Type, name: str) -> CustomEmoji: + """Edit an emoji for this application""" + return self.client.cache.place_emoji_data( + None, await self.client.http.edit_application_emoji(self.id, emoji_id, name) + ) + + async def delete_emoji(self, emoji_id: Snowflake_Type) -> None: + """Delete an emoji for this application""" + await self.client.http.delete_application_emoji(self.id, emoji_id) diff --git a/interactions/models/discord/emoji.py b/interactions/models/discord/emoji.py index 0364a7e5e..e9a91530c 100644 --- a/interactions/models/discord/emoji.py +++ b/interactions/models/discord/emoji.py @@ -38,6 +38,8 @@ class PartialEmoji(SnowflakeObject, DictSerializationMixin): """The custom emoji name, or standard unicode emoji in string""" animated: bool = attrs.field(repr=True, default=False) """Whether this emoji is animated""" + available: bool = attrs.field(repr=False, default=True) + """whether this emoji can be used, may be false due to loss of Server Boosts""" @classmethod def from_str(cls, emoji_str: str, *, language: str = "alias") -> Optional["PartialEmoji"]: @@ -120,7 +122,7 @@ class CustomEmoji(PartialEmoji, ClientObject): _role_ids: List["Snowflake_Type"] = attrs.field( repr=False, factory=list, converter=optional(list_converter(to_snowflake)) ) - _guild_id: "Snowflake_Type" = attrs.field(repr=False, default=None, converter=to_snowflake) + _guild_id: "Optional[Snowflake_Type]" = attrs.field(repr=False, default=None, converter=optional(to_snowflake)) @classmethod def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]: @@ -133,13 +135,13 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any] return data @classmethod - def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: int) -> "CustomEmoji": + def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: "Optional[Snowflake_Type]") -> "CustomEmoji": data = cls._process_dict(data, client) return cls(client=client, guild_id=guild_id, **cls._filter_kwargs(data, cls._get_init_keys())) @property - def guild(self) -> "Guild": - """The guild this emoji belongs to.""" + def guild(self) -> "Optional[Guild]": + """The guild this emoji belongs to, if applicable.""" return self._client.cache.get_guild(self._guild_id) @property @@ -160,6 +162,9 @@ def is_usable(self) -> bool: if not self.available: return False + if not self._guild_id: # likely an application emoji + return True + guild = self.guild return any(e_role_id in guild.me._role_ids for e_role_id in self._role_ids) @@ -182,14 +187,23 @@ async def edit( The newly modified custom emoji. """ - data_payload = dict_filter_none( - { - "name": name, - "roles": to_snowflake_list(roles) if roles else None, - } - ) + if self._guild_id: + data_payload = dict_filter_none( + { + "name": name, + "roles": to_snowflake_list(roles) if roles else None, + } + ) + + updated_data = await self._client.http.modify_guild_emoji( + data_payload, self._guild_id, self.id, reason=reason + ) + else: + if roles or reason: + raise ValueError("Cannot specify roles or reason for application emoji.") + + updated_data = await self.client.http.edit_application_emoji(self.bot.app.id, self.id, name) - updated_data = await self._client.http.modify_guild_emoji(data_payload, self._guild_id, self.id, reason=reason) self.update_from_dict(updated_data) return self @@ -202,9 +216,12 @@ async def delete(self, reason: Optional[str] = None) -> None: """ if not self._guild_id: - raise ValueError("Cannot delete emoji, no guild id set.") + if reason: + raise ValueError("Cannot specify reason for application emoji.") - await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason) + await self.client.http.delete_application_emoji(self._client.app.id, self.id) + else: + await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason) @property def url(self) -> str: diff --git a/tests/test_emoji.py b/tests/test_emoji.py index 5539e750f..58c826eda 100644 --- a/tests/test_emoji.py +++ b/tests/test_emoji.py @@ -34,7 +34,7 @@ def test_emoji_formatting() -> None: def test_emoji_processing() -> None: raw_sample = "<:sparklesnek:910496037708374016>" - dict_sample = {"id": 910496037708374016, "name": "sparklesnek", "animated": False} + dict_sample = {"id": 910496037708374016, "name": "sparklesnek", "animated": False, "available": True} unicode_sample = "👍" target = "sparklesnek:910496037708374016" @@ -48,7 +48,7 @@ def test_emoji_processing() -> None: assert isinstance(raw_emoji, dict) and raw_emoji == dict_sample assert isinstance(dict_emoji, dict) and dict_emoji == dict_sample - assert isinstance(unicode_emoji, dict) and unicode_emoji == {"name": "👍", "animated": False} + assert isinstance(unicode_emoji, dict) and unicode_emoji == {"name": "👍", "animated": False, "available": True} from_str = PartialEmoji.from_str(raw_sample) assert from_str.req_format == target