Skip to content

Commit caae85b

Browse files
silasaryAstreaTSS
andauthored
feat: Add support for Application Emoji (#1742)
* Feat: Add support for Application Emoji * Fix emoji tests * fix: make sure to await response Original code from #1746, which was closed for some reason. * feat: adjust methods in CustomEmoji to respect app emojis * fix: adjust app emoji code based off testing --------- Co-authored-by: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com>
1 parent 5b07e41 commit caae85b

File tree

5 files changed

+155
-18
lines changed

5 files changed

+155
-18
lines changed

interactions/api/http/http_requests/emojis.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,88 @@ async def delete_guild_emoji(
110110
Route("DELETE", "/guilds/{guild_id}/emojis/{emoji_id}", guild_id=guild_id, emoji_id=emoji_id),
111111
reason=reason,
112112
)
113+
114+
async def get_application_emojis(self, application_id: "Snowflake_Type") -> list[discord_typings.EmojiData]:
115+
"""
116+
Fetch all emojis for this application
117+
118+
Args:
119+
application_id: The id of the application
120+
121+
Returns:
122+
List of emojis
123+
124+
"""
125+
result = await self.request(Route("GET", f"/applications/{application_id}/emojis"))
126+
result = cast(dict, result)
127+
return cast(list[discord_typings.EmojiData], result["items"])
128+
129+
async def get_application_emoji(
130+
self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type"
131+
) -> discord_typings.EmojiData:
132+
"""
133+
Fetch an emoji for this application
134+
135+
Args:
136+
application_id: The id of the application
137+
emoji_id: The id of the emoji
138+
139+
Returns:
140+
Emoji object
141+
142+
"""
143+
result = await self.request(Route("GET", f"/applications/{application_id}/emojis/{emoji_id}"))
144+
return cast(discord_typings.EmojiData, result)
145+
146+
async def create_application_emoji(
147+
self, payload: dict, application_id: "Snowflake_Type", reason: str | None = None
148+
) -> discord_typings.EmojiData:
149+
"""
150+
Create an emoji for this application
151+
152+
Args:
153+
application_id: The id of the application
154+
name: The name of the emoji
155+
imagefile: The image file to use for the emoji
156+
157+
Returns:
158+
Emoji object
159+
160+
"""
161+
result = await self.request(
162+
Route("POST", f"/applications/{application_id}/emojis"), payload=payload, reason=reason
163+
)
164+
return cast(discord_typings.EmojiData, result)
165+
166+
async def edit_application_emoji(
167+
self, application_id: "Snowflake_Type", emoji_id: "Snowflake_Type", name: str
168+
) -> discord_typings.EmojiData:
169+
"""
170+
Edit an emoji for this application
171+
172+
Args:
173+
application_id: The id of the application
174+
emoji_id: The id of the emoji
175+
name: The new name for the emoji
176+
177+
Returns:
178+
Emoji object
179+
180+
"""
181+
result = await self.request(
182+
Route("PATCH", f"/applications/{application_id}/emojis/{emoji_id}"), payload={"name": name}
183+
)
184+
return cast(discord_typings.EmojiData, result)
185+
186+
async def delete_application_emoji(
187+
self, application_id: discord_typings.Snowflake, emoji_id: discord_typings.Snowflake
188+
) -> None:
189+
"""
190+
Delete an emoji for this application
191+
192+
Args:
193+
application_id: The id of the application
194+
emoji_id: The id of the emoji
195+
196+
"""
197+
await self.request(Route("DELETE", f"/applications/{application_id}/emojis/{emoji_id}"))

interactions/client/smart_cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ def place_guild_data(self, data: discord_typings.GuildData) -> Guild:
642642
643643
"""
644644
guild_id = to_snowflake(data["id"])
645-
guild: Guild = self.guild_cache.get(guild_id)
645+
guild: Guild | None = self.guild_cache.get(guild_id)
646646
if guild is None:
647647
guild = Guild.from_dict(data, self._client)
648648
self.guild_cache[guild_id] = guild
@@ -914,7 +914,7 @@ def get_emoji(self, emoji_id: Optional["Snowflake_Type"]) -> Optional["CustomEmo
914914
"""
915915
return self.emoji_cache.get(to_optional_snowflake(emoji_id)) if self.emoji_cache is not None else None
916916

917-
def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.EmojiData) -> "CustomEmoji":
917+
def place_emoji_data(self, guild_id: "Snowflake_Type | None", data: discord_typings.EmojiData) -> "CustomEmoji":
918918
"""
919919
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.
920920
@@ -929,7 +929,7 @@ def place_emoji_data(self, guild_id: "Snowflake_Type", data: discord_typings.Emo
929929
with suppress(KeyError):
930930
del data["guild_id"] # discord sometimes packages a guild_id - this will cause an exception
931931

932-
emoji = CustomEmoji.from_dict(data, self._client, to_snowflake(guild_id))
932+
emoji = CustomEmoji.from_dict(data, self._client, to_optional_snowflake(guild_id))
933933
if self.emoji_cache is not None:
934934
self.emoji_cache[emoji.id] = emoji
935935

interactions/models/discord/application.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
from interactions.client.const import MISSING
66
from interactions.client.utils.attr_converters import optional
7+
from interactions.client.utils.serializer import to_image_data
78
from interactions.models.discord.asset import Asset
9+
from interactions.models.discord.emoji import CustomEmoji
810
from interactions.models.discord.enums import ApplicationFlags
11+
from interactions.models.discord.file import UPLOADABLE_TYPE
912
from interactions.models.discord.snowflake import Snowflake_Type, to_snowflake
1013
from interactions.models.discord.team import Team
1114
from .base import DiscordObject
@@ -88,3 +91,35 @@ def _process_dict(cls, data: Dict[str, Any], client: "Client") -> Dict[str, Any]
8891
def owner(self) -> "User":
8992
"""The user object for the owner of this application"""
9093
return self._client.cache.get_user(self.owner_id)
94+
95+
async def fetch_all_emoji(self) -> List[CustomEmoji]:
96+
"""Fetch all emojis for this application"""
97+
response = await self.client.http.get_application_emojis(self.id)
98+
return [self.client.cache.place_emoji_data(None, emoji) for emoji in response]
99+
100+
async def fetch_emoji(self, emoji_id: Snowflake_Type) -> CustomEmoji:
101+
"""Fetch an emoji for this application"""
102+
response = await self.client.http.get_application_emoji(self.id, emoji_id)
103+
return self.client.cache.place_emoji_data(None, response)
104+
105+
async def create_emoji(self, name: str, imagefile: UPLOADABLE_TYPE) -> CustomEmoji:
106+
"""Create an emoji for this application"""
107+
data_payload = {
108+
"name": name,
109+
"image": to_image_data(imagefile),
110+
"roles": MISSING,
111+
}
112+
113+
return self.client.cache.place_emoji_data(
114+
None, await self.client.http.create_application_emoji(data_payload, self.id)
115+
)
116+
117+
async def edit_emoji(self, emoji_id: Snowflake_Type, name: str) -> CustomEmoji:
118+
"""Edit an emoji for this application"""
119+
return self.client.cache.place_emoji_data(
120+
None, await self.client.http.edit_application_emoji(self.id, emoji_id, name)
121+
)
122+
123+
async def delete_emoji(self, emoji_id: Snowflake_Type) -> None:
124+
"""Delete an emoji for this application"""
125+
await self.client.http.delete_application_emoji(self.id, emoji_id)

interactions/models/discord/emoji.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class PartialEmoji(SnowflakeObject, DictSerializationMixin):
3838
"""The custom emoji name, or standard unicode emoji in string"""
3939
animated: bool = attrs.field(repr=True, default=False)
4040
"""Whether this emoji is animated"""
41+
available: bool = attrs.field(repr=False, default=True)
42+
"""whether this emoji can be used, may be false due to loss of Server Boosts"""
4143

4244
@classmethod
4345
def from_str(cls, emoji_str: str, *, language: str = "alias") -> Optional["PartialEmoji"]:
@@ -120,7 +122,7 @@ class CustomEmoji(PartialEmoji, ClientObject):
120122
_role_ids: List["Snowflake_Type"] = attrs.field(
121123
repr=False, factory=list, converter=optional(list_converter(to_snowflake))
122124
)
123-
_guild_id: "Snowflake_Type" = attrs.field(repr=False, default=None, converter=to_snowflake)
125+
_guild_id: "Optional[Snowflake_Type]" = attrs.field(repr=False, default=None, converter=optional(to_snowflake))
124126

125127
@classmethod
126128
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]
133135
return data
134136

135137
@classmethod
136-
def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: int) -> "CustomEmoji":
138+
def from_dict(cls, data: Dict[str, Any], client: "Client", guild_id: "Optional[Snowflake_Type]") -> "CustomEmoji":
137139
data = cls._process_dict(data, client)
138140
return cls(client=client, guild_id=guild_id, **cls._filter_kwargs(data, cls._get_init_keys()))
139141

140142
@property
141-
def guild(self) -> "Guild":
142-
"""The guild this emoji belongs to."""
143+
def guild(self) -> "Optional[Guild]":
144+
"""The guild this emoji belongs to, if applicable."""
143145
return self._client.cache.get_guild(self._guild_id)
144146

145147
@property
@@ -160,6 +162,9 @@ def is_usable(self) -> bool:
160162
if not self.available:
161163
return False
162164

165+
if not self._guild_id: # likely an application emoji
166+
return True
167+
163168
guild = self.guild
164169
return any(e_role_id in guild.me._role_ids for e_role_id in self._role_ids)
165170

@@ -182,14 +187,23 @@ async def edit(
182187
The newly modified custom emoji.
183188
184189
"""
185-
data_payload = dict_filter_none(
186-
{
187-
"name": name,
188-
"roles": to_snowflake_list(roles) if roles else None,
189-
}
190-
)
190+
if self._guild_id:
191+
data_payload = dict_filter_none(
192+
{
193+
"name": name,
194+
"roles": to_snowflake_list(roles) if roles else None,
195+
}
196+
)
197+
198+
updated_data = await self._client.http.modify_guild_emoji(
199+
data_payload, self._guild_id, self.id, reason=reason
200+
)
201+
else:
202+
if roles or reason:
203+
raise ValueError("Cannot specify roles or reason for application emoji.")
204+
205+
updated_data = await self.client.http.edit_application_emoji(self.bot.app.id, self.id, name)
191206

192-
updated_data = await self._client.http.modify_guild_emoji(data_payload, self._guild_id, self.id, reason=reason)
193207
self.update_from_dict(updated_data)
194208
return self
195209

@@ -202,9 +216,12 @@ async def delete(self, reason: Optional[str] = None) -> None:
202216
203217
"""
204218
if not self._guild_id:
205-
raise ValueError("Cannot delete emoji, no guild id set.")
219+
if reason:
220+
raise ValueError("Cannot specify reason for application emoji.")
206221

207-
await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason)
222+
await self.client.http.delete_application_emoji(self._client.app.id, self.id)
223+
else:
224+
await self._client.http.delete_guild_emoji(self._guild_id, self.id, reason=reason)
208225

209226
@property
210227
def url(self) -> str:

tests/test_emoji.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_emoji_formatting() -> None:
3434

3535
def test_emoji_processing() -> None:
3636
raw_sample = "<:sparklesnek:910496037708374016>"
37-
dict_sample = {"id": 910496037708374016, "name": "sparklesnek", "animated": False}
37+
dict_sample = {"id": 910496037708374016, "name": "sparklesnek", "animated": False, "available": True}
3838
unicode_sample = "👍"
3939
target = "sparklesnek:910496037708374016"
4040

@@ -48,7 +48,7 @@ def test_emoji_processing() -> None:
4848

4949
assert isinstance(raw_emoji, dict) and raw_emoji == dict_sample
5050
assert isinstance(dict_emoji, dict) and dict_emoji == dict_sample
51-
assert isinstance(unicode_emoji, dict) and unicode_emoji == {"name": "👍", "animated": False}
51+
assert isinstance(unicode_emoji, dict) and unicode_emoji == {"name": "👍", "animated": False, "available": True}
5252

5353
from_str = PartialEmoji.from_str(raw_sample)
5454
assert from_str.req_format == target

0 commit comments

Comments
 (0)