Skip to content

feat(emoji)!: implement app emojis #1224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1223.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:attr:`Emoji.guild_id` can now be ``None`` is the emoji is owned by an application. You can use :meth:`Emoji.is_app_emoji` to check for that.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:attr:`Emoji.guild_id` can now be ``None`` is the emoji is owned by an application. You can use :meth:`Emoji.is_app_emoji` to check for that.
:attr:`Emoji.guild_id` can now be ``None`` if the emoji is owned by an application. You can use :meth:`Emoji.is_app_emoji` to check for that.

2 changes: 2 additions & 0 deletions changelog/1223.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Edit :class:`.Emoji` to represent application emojis.
Add new methods and properties on :class:`Client` to fetch and create application emojis: :meth:`Client.fetch_application_emoji`, :meth:`Client.fetch_application_emojis` and :meth:`Client.create_application_emoji`.
86 changes: 86 additions & 0 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2420,6 +2420,92 @@ async def application_info(self) -> AppInfo:
data["rpc_origins"] = None
return AppInfo(self._connection, data)

async def fetch_application_emoji(self, emoji_id: int) -> Emoji:
"""|coro|

Retrieves an application level :class:`~disnake.Emoji` based on its ID.

.. versionadded:: 2.11

Parameters
----------
emoji_id: :class:`int`
The ID of the emoji to retrieve.

Raises
------
NotFound
The app emoji couldn't be found.
Forbidden
You are not allowed to get the app emoji.

Returns
-------
:class:`.Emoji`
The application emoji you requested.
"""
data = await self.http.get_app_emoji(self.application_id, emoji_id)
return Emoji(guild=None, state=self._connection, data=data)

async def create_application_emoji(self, *, name: str, image: AssetBytes) -> Emoji:
"""|coro|

Creates an application emoji.

.. versionadded:: 2.11

Parameters
----------
name: :class:`str`
The emoji name. Must be at least 2 characters.
image: |resource_type|
The image data of the emoji.
Only JPG, PNG and GIF images are supported.

Raises
------
NotFound
The ``image`` asset couldn't be found.
Forbidden
You are not allowed to create app emojis.
HTTPException
An error occurred creating an app emoji.
TypeError
The ``image`` asset is a lottie sticker (see :func:`Sticker.read <disnake.Sticker.read>`).
ValueError
Wrong image format passed for ``image``.

Returns
-------
:class:`.Emoji`
The newly created application emoji.
"""
img = await utils._assetbytes_to_base64_data(image)
data = await self.http.create_app_emoji(self.application_id, name, img)
return Emoji(guild=None, state=self._connection, data=data)

async def fetch_application_emojis(self) -> List[Emoji]:
"""|coro|

Retrieves all the :class:`.Emoji` of the application.

.. versionadded:: 2.11

Raises
------
NotFound
The app emojis for this application ID couldn't be found.
Forbidden
You are not allowed to get app emojis.

Returns
-------
List[:class:`.Emoji`]
The list of application emojis you requested.
"""
data = await self.http.get_all_app_emojis(self.application_id)
return [Emoji(guild=None, state=self._connection, data=emoji_data) for emoji_data in data]

async def fetch_user(self, user_id: int, /) -> User:
"""|coro|

Expand Down
72 changes: 61 additions & 11 deletions disnake/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class Emoji(_EmojiTag, AssetMixin):

Returns the emoji rendered for Discord.

.. versionchanged:: 2.11

This class can now represents app emojis too. You can use :meth:`Emoji.is_app_emoji` to check for this.

Attributes
----------
name: :class:`str`
Expand All @@ -63,8 +67,8 @@ 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.
guild_id: Optional[:class:`int`]
The guild ID the emoji belongs to. ``None`` if this is an app emoji.
available: :class:`bool`
Whether the emoji is available for use.
user: Optional[:class:`User`]
Expand All @@ -86,9 +90,13 @@ class Emoji(_EmojiTag, AssetMixin):
)

def __init__(
self, *, guild: Union[Guild, GuildPreview], state: ConnectionState, data: EmojiPayload
self,
*,
guild: Optional[Union[Guild, GuildPreview]],
state: ConnectionState,
data: EmojiPayload,
) -> None:
self.guild_id: int = guild.id
self.guild_id: Optional[int] = guild.id if guild else None
self._state: ConnectionState = state
self._from_data(data)

Expand Down Expand Up @@ -151,16 +159,42 @@ def roles(self) -> List[Role]:
and count towards a separate limit of 25 emojis.
"""
guild = self.guild
if guild is None: # pyright: ignore[reportUnnecessaryComparison]
if guild is None:
return []

return [role for role in guild.roles if self._roles.has(role.id)]

@property
def guild(self) -> Guild:
""":class:`Guild`: The guild this emoji belongs to."""
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild this emoji belongs to. ``None`` if this is an app emoji.

.. versionchanged:: 2.11

This can now return ``None`` if the emoji is an
application owned emoji.
"""
# this will most likely never return None but there's a possibility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is no longer saying anything useful, remove it perhaps?

return self._state._get_guild(self.guild_id) # type: ignore
return self._state._get_guild(self.guild_id)

@property
def application_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of the application which owns this emoji.

.. versionadded:: 2.11
"""
if self.guild_id is None:
return None
return self._state.application_id

@property
def is_app_emoji(self) -> bool:
""":class:`bool`: Whether this is an application emoji.

.. versionadded:: 2.11
"""
if self.guild_id is None:
return True
return False
Comment on lines +195 to +197
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self.guild_id is None:
return True
return False
return self.guild_id is None


def is_usable(self) -> bool:
"""Whether the bot can use this emoji.
Expand All @@ -173,6 +207,8 @@ def is_usable(self) -> bool:
return False
if not self._roles:
return True
if not self.guild:
return self.available
emoji_roles, my_roles = self._roles, self.guild.me._roles
return any(my_roles.has(role_id) for role_id in emoji_roles)

Expand All @@ -196,6 +232,13 @@ async def delete(self, *, reason: Optional[str] = None) -> None:
HTTPException
An error occurred deleting the emoji.
"""
# this is an app emoji
if self.guild is None:
if self.application_id is None:
# should never happen
raise ValueError("This may be a library bug! Open an issue on GitHub.")

return await self._state.http.delete_app_emoji(self.application_id, self.id)
await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason)

async def edit(
Expand Down Expand Up @@ -242,7 +285,14 @@ async def edit(
if roles is not MISSING:
payload["roles"] = [role.id for role in roles]

data = await self._state.http.edit_custom_emoji(
self.guild.id, self.id, payload=payload, reason=reason
)
if self.guild is None:
if self.application_id is None:
# should never happen
raise ValueError("This may be a library bug! Open an issue on GitHub.")

data = await self._state.http.edit_app_emoji(self.application_id, self.id, name)
else:
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)
49 changes: 49 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,16 @@ def delete_guild_sticker(
reason=reason,
)

def get_all_app_emojis(self, app_id: Snowflake) -> Response[List[emoji.Emoji]]:
return self.request(Route("GET", "/applications/{app_id}/emojis", app_id=app_id))

def get_app_emoji(self, app_id: Snowflake, emoji_id: Snowflake) -> Response[emoji.Emoji]:
return self.request(
Route(
"GET", "/applications/{app_id}/emojis/{emoji_id}", app_id=app_id, emoji_id=emoji_id
)
)

def get_all_custom_emojis(self, guild_id: Snowflake) -> Response[List[emoji.Emoji]]:
return self.request(Route("GET", "/guilds/{guild_id}/emojis", guild_id=guild_id))

Expand All @@ -1735,6 +1745,45 @@ def get_custom_emoji(self, guild_id: Snowflake, emoji_id: Snowflake) -> Response
)
)

def create_app_emoji(
self,
app_id: Snowflake,
name: str,
image: str,
) -> Response[emoji.Emoji]:
payload: Dict[str, Any] = {
"name": name,
"image": image,
}

r = Route("POST", "/applications/{app_id}/emojis", app_id=app_id)
return self.request(r, json=payload)

def edit_app_emoji(
self,
app_id: Snowflake,
emoji_id: Snowflake,
name: str,
) -> Response[emoji.Emoji]:
payload: Dict[str, Any] = {
"name": name,
}

r = Route(
"PATCH", "/applications/{app_id}/emojis/{emoji_id}", app_id=app_id, emoji_id=emoji_id
)
return self.request(r, json=payload)

def delete_app_emoji(self, app_id: Snowflake, emoji_id: Snowflake) -> Response[None]:
return self.request(
Route(
"DELETE",
"/applications/{app_id}/emojis/{emoji_id}",
app_id=app_id,
emoji_id=emoji_id,
)
)

def create_custom_emoji(
self,
guild_id: Snowflake,
Expand Down
Loading