Skip to content
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

feat(emoji): implement app emojis #1224

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
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 get, fetch and create application emojis: :meth:`Client.get_application_emoji`, :meth:`Client.fetch_application_emoji`, :meth:`Client.get_or_fetch_application_emoji`, :meth:`Client.getch_application_emoji`, :meth:`Client.fetch_application_emojis`, :attr:`Client.application_emojis` and :meth:`Client.create_application_emoji`.
195 changes: 195 additions & 0 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ def __init__(
intents: Optional[Intents] = None,
chunk_guilds_at_startup: Optional[bool] = None,
member_cache_flags: Optional[MemberCacheFlags] = None,
cache_app_emojis_at_startup: bool = False,
) -> None:
# self.ws is set in the connect method
self.ws: DiscordWebSocket = None # type: ignore
Expand Down Expand Up @@ -444,6 +445,7 @@ def __init__(
intents=intents,
chunk_guilds_at_startup=chunk_guilds_at_startup,
member_cache_flags=member_cache_flags,
cache_app_emojis_at_startup=cache_app_emojis_at_startup,
)
self.shard_id: Optional[int] = shard_id
self.shard_count: Optional[int] = shard_count
Expand Down Expand Up @@ -498,6 +500,7 @@ def _get_state(
intents: Optional[Intents],
chunk_guilds_at_startup: Optional[bool],
member_cache_flags: Optional[MemberCacheFlags],
cache_app_emojis_at_startup: bool,
) -> ConnectionState:
return ConnectionState(
dispatch=self.dispatch,
Expand All @@ -515,6 +518,7 @@ def _get_state(
intents=intents,
chunk_guilds_at_startup=chunk_guilds_at_startup,
member_cache_flags=member_cache_flags,
cache_app_emojis_at_startup=cache_app_emojis_at_startup,
)

def _handle_ready(self) -> None:
Expand Down Expand Up @@ -563,6 +567,14 @@ def emojis(self) -> List[Emoji]:
"""List[:class:`.Emoji`]: The emojis that the connected client has."""
return self._connection.emojis

@property
def application_emojis(self) -> List[Emoji]:
"""List[:class:`.Emoji`]: The application emojis that the connected client has.

.. versionadded:: 2.10
"""
return self._connection.application_emojis

@property
def stickers(self) -> List[GuildSticker]:
"""List[:class:`.GuildSticker`]: The stickers that the connected client has.
Expand Down Expand Up @@ -711,6 +723,58 @@ async def get_or_fetch_user(self, user_id: int, *, strict: bool = False) -> Opti

getch_user = get_or_fetch_user

@overload
async def get_or_fetch_application_emoji(
self, emoji_id: int, *, strict: Literal[False] = ...
) -> Optional[Emoji]:
...

@overload
async def get_or_fetch_application_emoji(
self, emoji_id: int, *, strict: Literal[True] = ...
) -> Emoji:
...

async def get_or_fetch_application_emoji(
self, emoji_id: int, *, strict: bool = False
) -> Optional[Emoji]:
"""|coro|

Tries to get the application emoji from the cache. If it fails,
fetches the app emoji from the API.

This only propagates exceptions when the ``strict`` parameter is enabled.

.. versionadded:: 2.10

Parameters
----------
emoji_id: :class:`int`
The ID to search for.
strict: :class:`bool`
Whether to propagate exceptions from :func:`fetch_application_emoji`
instead of returning ``None`` in case of failure
(e.g. if the app emoji wasn't found).
Defaults to ``False``.

Returns
-------
Optional[:class:`~disnake.Emoji`]
The app emoji with the given ID, or ``None`` if not found and ``strict`` is set to ``False``.
"""
app_emoji = self.get_application_emoji(emoji_id)
if app_emoji is not None:
return app_emoji
try:
app_emoji = await self.fetch_application_emoji(emoji_id)
except Exception:
if strict:
raise
return None
return app_emoji

getch_application_emoji = get_or_fetch_application_emoji

def is_ready(self) -> bool:
"""Whether the client's internal cache is ready for use.

Expand Down Expand Up @@ -1489,6 +1553,28 @@ def get_emoji(self, id: int, /) -> Optional[Emoji]:
"""
return self._connection.get_emoji(id)

def get_application_emoji(self, emoji_id: int, /) -> Optional[Emoji]:
"""Returns an application emoji with the given ID.

.. versionadded:: 2.10

.. note::

If this returns ``None`` consider executing :meth:`fetch_application_emoji`
or enable :attr:`disnake.Client.cache_app_emoji`.

Parameters
----------
emoji_id: :class:`int`
The ID to search for.

Returns
-------
Optional[:class:`.Emoji`]
The returned application emoji or ``None`` if not found.
"""
return self._connection.get_application_emoji(emoji_id)

def get_sticker(self, id: int, /) -> Optional[GuildSticker]:
"""Returns a guild sticker with the given ID.

Expand Down Expand Up @@ -2372,6 +2458,115 @@ async def application_info(self) -> AppInfo:
data["rpc_origins"] = None
return AppInfo(self._connection, data)

async def fetch_application_emoji(self, emoji_id: int, /, cache: bool = False) -> Emoji:
"""|coro|

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

.. note::

This method is an API call. If you have :attr:`disnake.Client.cache_application_emojis` enabled, consider :meth:`get_application_emoji` instead.

.. versionadded:: 2.10

Parameters
----------
emoji_id: :class:`int`
The ID of the emoji to retrieve.
cache: :class:`bool`
Whether to update the cache.

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)

if cache:
return self._connection.store_application_emoji(data=data)
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.10

Parameters
----------
name: :class:`str`
The name of the new string.
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
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`).
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
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 self._connection.store_application_emoji(data)

async def fetch_application_emojis(self, *, cache: bool = False) -> List[Emoji]:
"""|coro|

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

.. versionadded:: 2.10

Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
cache: :class:`bool`
Whether to update the cache.

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)

if cache:
app_emojis = []
for emoji_data in data:
app_emojis.append(self._connection.store_application_emoji(emoji_data))

return app_emojis

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
64 changes: 53 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.

.. versionchanched:: 2.10
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved

This class can now represents app emojis too. Denoted by having :attr:`.Emoji.guild_id` as ``None``.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a is_app_emoji property would be nice here


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,34 @@ 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.10

This can now return ``None`` if the emoji is an
application owned emoji.
"""
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
# this will most likely never return None but there's a possibility
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]:
""":class:`int`: The ID of the application which owns this emoji.
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 2.10
"""
if self.guild is None:
return
Snipy7374 marked this conversation as resolved.
Show resolved Hide resolved
return self._state.application_id

def is_usable(self) -> bool:
"""Whether the bot can use this emoji.
Expand All @@ -173,6 +199,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 +224,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("Idk message about invalid state?! Pls catch this when reviewing")

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 +277,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("Idk message about invalid state?! Pls catch this when reviewing")

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)
Loading
Loading