Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6ef3bc0
implement app emojis
Snipy7374 Aug 10, 2024
cae204a
add changelog file
Snipy7374 Aug 10, 2024
ca00391
update docs
Snipy7374 Aug 10, 2024
c1f492a
Merge branch 'master' into feat/app_emojis
Snipy7374 Aug 10, 2024
18a127b
Merge branch 'master' into feat/app_emojis
Snipy7374 Aug 21, 2024
710c40f
edit Emoji to represent app emojis
Snipy7374 Aug 21, 2024
d405d7f
fix docs
Snipy7374 Aug 21, 2024
d397f8d
don't store application id in emoji
Snipy7374 Aug 21, 2024
37f218d
update slots
Snipy7374 Aug 21, 2024
51dae3b
Update disnake/client.py
Snipy7374 Nov 26, 2024
b2d343a
Update disnake/client.py
Snipy7374 Nov 26, 2024
9cba32c
Update disnake/emoji.py
Snipy7374 Nov 26, 2024
3b3cbb9
Update disnake/emoji.py
Snipy7374 Nov 26, 2024
8247fa3
Update disnake/emoji.py
Snipy7374 Nov 26, 2024
e38bda8
Update disnake/client.py
Snipy7374 Nov 26, 2024
74bdc75
Update disnake/emoji.py
Snipy7374 Nov 26, 2024
0e7ad20
Merge branch 'master' into feat/app_emojis
Snipy7374 Nov 26, 2024
2d94bc5
Merge branch 'master' into feat/app_emojis
Snipy7374 Apr 14, 2025
3c5a6ef
remove caching capabilities for app emojis
Snipy7374 Apr 14, 2025
4170cba
bump versions, add is_app_emoji property and fix docs
Snipy7374 Apr 14, 2025
47aec98
update changelog entry
Snipy7374 Apr 14, 2025
4b9cef8
add breaking change notice
Snipy7374 Apr 14, 2025
fc3276a
make properties use guild_id attribute instead of calling other 2 fun…
Snipy7374 Apr 14, 2025
4c7ff52
Update changelog/1223.breaking.rst
Snipy7374 Aug 22, 2025
95d2b3b
Update disnake/emoji.py
Snipy7374 Aug 22, 2025
18fa4e0
remove useless comment
Snipy7374 Aug 22, 2025
92898df
Merge branch 'master' into feat/app_emojis
onerandomusername Sep 9, 2025
17d9888
Merge branch 'master' into feat/app_emojis
onerandomusername Sep 16, 2025
9680d53
fix: update docstrings to use |vnext|
onerandomusername Sep 16, 2025
bafa0b8
fix: raise InvalidData instead of TypeError
onerandomusername Sep 16, 2025
97f478d
refactor: check self.guild before _roles for is_usable
onerandomusername Sep 16, 2025
ad93f1f
feat: add is_guild_emoji
onerandomusername Sep 16, 2025
3aca198
fix: list all app emojis returns a dict
onerandomusername Sep 16, 2025
ffb7bde
fix: emojis have application_id only if guild_id is None
onerandomusername Sep 16, 2025
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`` if the emoji is owned by an application. You can use :attr:`Emoji.is_guild_emoji` and :attr:`Emoji.is_app_emoji` to check if this is a Guild or App Emoji.
3 changes: 3 additions & 0 deletions changelog/1223.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add support to :class:`.Emoji` to represent application emojis.
New methods on :class:`Client`: :meth:`Client.fetch_application_emoji`, :meth:`Client.fetch_application_emojis` and :meth:`Client.create_application_emoji`.
New attributes on :class:`.Emoji`: :attr:`Emoji.application_id`, :attr:`Emoji.is_guild_emoji` and :attr:`Emoji.is_app_emoji`.
89 changes: 89 additions & 0 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2418,6 +2418,95 @@ async def application_info(self) -> AppInfo:
data = await self.http.application_info()
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:: |vnext|

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:: |vnext|

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:: |vnext|

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["items"]
]

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

Expand Down
95 changes: 82 additions & 13 deletions disnake/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union

from .asset import Asset, AssetMixin
from .errors import InvalidData
from .partial_emoji import PartialEmoji, _EmojiTag
from .user import User
from .utils import MISSING, SnowflakeList, snowflake_time
Expand Down Expand Up @@ -51,6 +52,11 @@ class Emoji(_EmojiTag, AssetMixin):

Returns the emoji rendered for Discord.

.. versionchanged:: |vnext|

This class can now represents app emojis. Use :attr:`Emoji.is_app_emoji` to check for this.
To check if this is a guild emoji, use :attr:`Emoji.is_guild_emoji`.

Attributes
----------
name: :class:`str`
Expand All @@ -63,8 +69,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 +92,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 @@ -119,7 +129,12 @@ def __str__(self) -> str:
return f"<:{self.name}:{self.id}>"

def __repr__(self) -> str:
return f"<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>"
return (
f"<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed} "
+ (f"{self.guild_id=}" if self.guild_id else "")
+ (f"{self.application_id=}" if self.application_id else "")
+ ">"
)

def __eq__(self, other: Any) -> bool:
return isinstance(other, _EmojiTag) and self.id == other.id
Expand Down Expand Up @@ -151,16 +166,47 @@ 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."""
# this will most likely never return None but there's a possibility
return self._state._get_guild(self.guild_id) # type: ignore
def guild(self) -> Optional[Guild]:
"""Optional[:class:`Guild`]: The guild this emoji belongs to. ``None`` if this is an app emoji.

.. versionchanged:: |vnext|

This can now return ``None`` if the emoji is an
application owned emoji.
"""
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:: |vnext|
"""
if self.guild_id:
return None
return self._state.application_id

@property
def is_guild_emoji(self) -> bool:
""":class:`bool`: Whether this is a guild emoji.

.. versionadded:: |vnext|
"""
return self.guild_id is not None

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

.. versionadded:: |vnext|
"""
return self.guild_id is None

def is_usable(self) -> bool:
"""Whether the bot can use this emoji.
Expand All @@ -171,6 +217,9 @@ def is_usable(self) -> bool:
"""
if not self.available:
return False
if not self.guild:
# if we don't have a guild, this is an app emoji
return self.available
if not self._roles:
return True
emoji_roles, my_roles = self._roles, self.guild.me._roles
Expand All @@ -195,7 +244,17 @@ async def delete(self, *, reason: Optional[str] = None) -> None:
You are not allowed to delete this emoji.
HTTPException
An error occurred deleting the emoji.
InvalidData
The emoji data is invalid and cannot be processed.
"""
# this is an app emoji
if self.guild is None:
if self.application_id is None:
# should never happen
msg = f"guild and application_id are both None when attempting to delete emoji with ID {self.id} This may be a library bug! Open an issue on GitHub."
raise InvalidData(msg)

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 @@ -230,6 +289,8 @@ async def edit(
You are not allowed to edit this emoji.
HTTPException
An error occurred editing the emoji.
InvalidData
The emoji data is invalid and cannot be processed.

Returns
-------
Expand All @@ -242,7 +303,15 @@ 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
msg = f"guild and application_id are both None when attempting to edit emoji with ID {self.id} This may be a library bug! Open an issue on GitHub."
raise InvalidData(msg)

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)
41 changes: 41 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,16 @@ def delete_guild_sticker(
reason=reason,
)

def get_all_app_emojis(self, app_id: Snowflake) -> Response[emoji.ListAppEmoji]:
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 @@ -1760,6 +1770,37 @@ 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
6 changes: 5 additions & 1 deletion disnake/types/emoji.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,3 +23,7 @@ class Emoji(PartialEmoji, total=False):
class EditEmoji(TypedDict):
name: str
roles: Optional[SnowflakeList]


class ListAppEmoji(TypedDict):
items: List[Emoji]
Loading