aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNadir Chowdhury <[email protected]>2021-07-31 02:25:41 +0100
committerGitHub <[email protected]>2021-07-30 21:25:41 -0400
commit60d82cf908e537e4ea5f068a31377452a9d6db3d (patch)
tree9632e3e99759ad5818047a5132ca39afa30c0749
parentFix user cache acting incorrectly with evictions (diff)
downloaddiscord.py-60d82cf908e537e4ea5f068a31377452a9d6db3d.tar.xz
discord.py-60d82cf908e537e4ea5f068a31377452a9d6db3d.zip
implement guild stickers
-rw-r--r--discord/abc.py21
-rw-r--r--discord/asset.py6
-rw-r--r--discord/audit_logs.py27
-rw-r--r--discord/client.py69
-rw-r--r--discord/enums.py25
-rw-r--r--discord/flags.py18
-rw-r--r--discord/guild.py174
-rw-r--r--discord/http.py64
-rw-r--r--discord/message.py8
-rw-r--r--discord/permissions.py24
-rw-r--r--discord/state.py29
-rw-r--r--discord/sticker.py456
-rw-r--r--discord/types/audit_log.py9
-rw-r--r--discord/types/message.py19
-rw-r--r--discord/types/sticker.py93
-rw-r--r--docs/api.rst162
16 files changed, 1119 insertions, 85 deletions
diff --git a/discord/abc.py b/discord/abc.py
index a2f4a847..2ed7b513 100644
--- a/discord/abc.py
+++ b/discord/abc.py
@@ -31,12 +31,11 @@ from typing import (
Callable,
Dict,
List,
- Mapping,
Optional,
TYPE_CHECKING,
Protocol,
+ Sequence,
Tuple,
- Type,
TypeVar,
Union,
overload,
@@ -53,6 +52,7 @@ from .role import Role
from .invite import Invite
from .file import File
from .voice_client import VoiceClient, VoiceProtocol
+from .sticker import GuildSticker, StickerItem
from . import utils
__all__ = (
@@ -1164,6 +1164,7 @@ class Messageable:
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
+ stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
@@ -1181,6 +1182,7 @@ class Messageable:
tts: bool = ...,
embed: Embed = ...,
files: List[File] = ...,
+ stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
@@ -1198,6 +1200,7 @@ class Messageable:
tts: bool = ...,
embeds: List[Embed] = ...,
file: File = ...,
+ stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
@@ -1215,6 +1218,7 @@ class Messageable:
tts: bool = ...,
embeds: List[Embed] = ...,
files: List[File] = ...,
+ stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
allowed_mentions: AllowedMentions = ...,
@@ -1233,6 +1237,7 @@ class Messageable:
embeds=None,
file=None,
files=None,
+ stickers=None,
delete_after=None,
nonce=None,
allowed_mentions=None,
@@ -1305,6 +1310,10 @@ class Messageable:
A list of embeds to upload. Must be a maximum of 10.
.. versionadded:: 2.0
+ stickers: Sequence[Union[:class:`GuildSticker`, :class:`StickerItem`]]
+ A list of stickers to upload. Must be a maximum of 3.
+
+ .. versionadded:: 2.0
Raises
--------
@@ -1340,6 +1349,9 @@ class Messageable:
raise InvalidArgument('embeds parameter must be a list of up to 10 elements')
embeds = [embed.to_dict() for embed in embeds]
+ if stickers is not None:
+ stickers = [sticker.id for sticker in stickers]
+
if allowed_mentions is not None:
if state.allowed_mentions is not None:
allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict()
@@ -1384,6 +1396,7 @@ class Messageable:
embeds=embeds,
nonce=nonce,
message_reference=reference,
+ stickers=stickers,
components=components,
)
finally:
@@ -1406,6 +1419,7 @@ class Messageable:
nonce=nonce,
allowed_mentions=allowed_mentions,
message_reference=reference,
+ stickers=stickers,
components=components,
)
finally:
@@ -1421,6 +1435,7 @@ class Messageable:
nonce=nonce,
allowed_mentions=allowed_mentions,
message_reference=reference,
+ stickers=stickers,
components=components,
)
@@ -1454,7 +1469,7 @@ class Messageable:
This means that both ``with`` and ``async with`` work with this.
Example Usage: ::
-
+
async with channel.typing():
# simulate something heavy
await asyncio.sleep(10)
diff --git a/discord/asset.py b/discord/asset.py
index eafff466..8aa43914 100644
--- a/discord/asset.py
+++ b/discord/asset.py
@@ -216,11 +216,11 @@ class Asset(AssetMixin):
)
@classmethod
- def _from_sticker(cls, state, sticker_id: int, sticker_hash: str) -> Asset:
+ def _from_sticker_banner(cls, state, banner: int) -> Asset:
return cls(
state,
- url=f'{cls.BASE}/stickers/{sticker_id}/{sticker_hash}.png?size=1024',
- key=sticker_hash,
+ url=f'{cls.BASE}/app-assets/710982414301790216/store/{banner}.png',
+ key=str(banner),
animated=False,
)
diff --git a/discord/audit_logs.py b/discord/audit_logs.py
index 2c55370e..1870620f 100644
--- a/discord/audit_logs.py
+++ b/discord/audit_logs.py
@@ -58,6 +58,7 @@ if TYPE_CHECKING:
from .types.snowflake import Snowflake
from .user import User
from .stage_instance import StageInstance
+ from .sticker import GuildSticker
from .threads import Thread
@@ -79,16 +80,15 @@ def _transform_channel(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optio
return entry.guild.get_channel(int(data)) or Object(id=data)
-def _transform_owner_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
+def _transform_member_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
if data is None:
return None
return entry._get_member(int(data))
-
-def _transform_inviter_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Union[Member, User, None]:
+def _transform_guild_id(entry: AuditLogEntry, data: Optional[Snowflake]) -> Optional[Guild]:
if data is None:
return None
- return entry._get_member(int(data))
+ return entry._state._get_guild(data)
def _transform_overwrites(
@@ -146,6 +146,11 @@ def _enum_transformer(enum: Type[T]) -> Callable[[AuditLogEntry, int], T]:
return _transform
+def _transform_type(entry: AuditLogEntry, data: Union[int]) -> Union[enums.ChannelType, enums.StickerType]:
+ if entry.action.name.startswith('sticker_'):
+ return enums.try_enum(enums.StickerType, data)
+ else:
+ return enums.try_enum(enums.ChannelType, data)
class AuditLogDiff:
def __len__(self) -> int:
@@ -180,8 +185,8 @@ class AuditLogChanges:
'permissions': (None, _transform_permissions),
'id': (None, _transform_snowflake),
'color': ('colour', _transform_color),
- 'owner_id': ('owner', _transform_owner_id),
- 'inviter_id': ('inviter', _transform_inviter_id),
+ 'owner_id': ('owner', _transform_member_id),
+ 'inviter_id': ('inviter', _transform_member_id),
'channel_id': ('channel', _transform_channel),
'afk_channel_id': ('afk_channel', _transform_channel),
'system_channel_id': ('system_channel', _transform_channel),
@@ -195,12 +200,15 @@ class AuditLogChanges:
'icon_hash': ('icon', _transform_icon),
'avatar_hash': ('avatar', _transform_avatar),
'rate_limit_per_user': ('slowmode_delay', None),
+ 'guild_id': ('guild', _transform_guild_id),
+ 'tags': ('emoji', None),
'default_message_notifications': ('default_notifications', _enum_transformer(enums.NotificationLevel)),
'region': (None, _enum_transformer(enums.VoiceRegion)),
'rtc_region': (None, _enum_transformer(enums.VoiceRegion)),
'video_quality_mode': (None, _enum_transformer(enums.VideoQualityMode)),
'privacy_level': (None, _enum_transformer(enums.StagePrivacyLevel)),
- 'type': (None, _enum_transformer(enums.ChannelType)),
+ 'format_type': (None, _enum_transformer(enums.StickerFormatType)),
+ 'type': (None, _transform_type),
}
# fmt: on
@@ -438,7 +446,7 @@ class AuditLogEntry(Hashable):
return utils.snowflake_time(self.id)
@utils.cached_property
- def target(self) -> Union[Guild, abc.GuildChannel, Member, User, Role, Invite, Emoji, Object, Thread, None]:
+ def target(self) -> Union[Guild, abc.GuildChannel, Member, User, Role, Invite, Emoji, StageInstance, GuildSticker, Thread, Object, None]:
try:
converter = getattr(self, '_convert_target_' + self.action.target_type)
except AttributeError:
@@ -509,5 +517,8 @@ class AuditLogEntry(Hashable):
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:
return self.guild.get_stage_instance(target_id) or Object(id=target_id)
+ def _convert_target_sticker(self, target_id: int) -> Union[GuildSticker, Object]:
+ return self._state.get_sticker(target_id) or Object(id=target_id)
+
def _convert_target_thread(self, target_id: int) -> Union[Thread, Object]:
return self.guild.get_thread(target_id) or Object(id=target_id)
diff --git a/discord/client.py b/discord/client.py
index 298dff3a..9c5c7dbf 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -60,6 +60,7 @@ from .appinfo import AppInfo
from .ui.view import View
from .stage_instance import StageInstance
from .threads import Thread
+from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory
if TYPE_CHECKING:
from .abc import SnowflakeTime, PrivateChannel, GuildChannel, Snowflake
@@ -278,6 +279,14 @@ class Client:
return self._connection.emojis
@property
+ def stickers(self) -> List[GuildSticker]:
+ """List[:class:`GuildSticker`]: The stickers that the connected client has.
+
+ .. versionadded:: 2.0
+ """
+ return self._connection.stickers
+
+ @property
def cached_messages(self) -> Sequence[Message]:
"""Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached.
@@ -777,6 +786,23 @@ class Client:
"""
return self._connection.get_emoji(id)
+ def get_sticker(self, id: int) -> Optional[GuildSticker]:
+ """Returns a guild sticker with the given ID.
+
+ .. versionadded:: 2.0
+
+ .. note::
+
+ To retrieve standard stickers, use :meth:`.fetch_sticker`.
+ or :meth:`.fetch_nitro_sticker_packs`.
+
+ Returns
+ --------
+ Optional[:class:`.GuildSticker`]
+ The sticker or ``None`` if not found.
+ """
+ return self._connection.get_sticker(id)
+
def get_all_channels(self) -> Generator[GuildChannel, None, None]:
"""A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'.
@@ -1443,6 +1469,49 @@ class Client:
data = await self.http.get_webhook(webhook_id)
return Webhook.from_state(data, state=self._connection)
+ async def fetch_sticker(self, sticker_id: int) -> Union[StandardSticker, GuildSticker]:
+ """|coro|
+
+ Retrieves a :class:`.Sticker` with the specified ID.
+
+ .. versionadded:: 2.0
+
+ Raises
+ --------
+ :exc:`.HTTPException`
+ Retrieving the sticker failed.
+ :exc:`.NotFound`
+ Invalid sticker ID.
+
+ Returns
+ --------
+ Union[:class:`.StandardSticker`, :class:`.GuildSticker`]
+ The sticker you requested.
+ """
+ data = await self.http.get_sticker(sticker_id)
+ cls, _ = _sticker_factory(data['type']) # type: ignore
+ return cls(state=self._connection, data=data) # type: ignore
+
+ async def fetch_nitro_sticker_packs(self) -> List[StickerPack]:
+ """|coro|
+
+ Retrieves all available nitro sticker packs.
+
+ .. versionadded:: 2.0
+
+ Raises
+ -------
+ :exc:`.HTTPException`
+ Retrieving the sticker packs failed.
+
+ Returns
+ ---------
+ List[:class:`.StickerPack`]
+ All available nitro sticker packs.
+ """
+ data = await self.http.list_nitro_sticker_packs()
+ return [StickerPack(state=self._connection, data=pack) for pack in data['sticker_packs']]
+
async def create_dm(self, user: Snowflake) -> DMChannel:
"""|coro|
diff --git a/discord/enums.py b/discord/enums.py
index d2e682f3..80af39ec 100644
--- a/discord/enums.py
+++ b/discord/enums.py
@@ -46,6 +46,7 @@ __all__ = (
'ExpireBehaviour',
'ExpireBehavior',
'StickerType',
+ 'StickerFormatType',
'InviteTarget',
'VideoQualityMode',
'ComponentType',
@@ -346,6 +347,9 @@ class AuditLogAction(Enum):
stage_instance_create = 83
stage_instance_update = 84
stage_instance_delete = 85
+ sticker_create = 90
+ sticker_update = 91
+ sticker_delete = 92
thread_create = 110
thread_update = 111
thread_delete = 112
@@ -393,6 +397,9 @@ class AuditLogAction(Enum):
AuditLogAction.stage_instance_create: AuditLogActionCategory.create,
AuditLogAction.stage_instance_update: AuditLogActionCategory.update,
AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete,
+ AuditLogAction.sticker_create: AuditLogActionCategory.create,
+ AuditLogAction.sticker_update: AuditLogActionCategory.update,
+ AuditLogAction.sticker_delete: AuditLogActionCategory.delete,
AuditLogAction.thread_create: AuditLogActionCategory.create,
AuditLogAction.thread_update: AuditLogActionCategory.update,
AuditLogAction.thread_delete: AuditLogActionCategory.delete,
@@ -427,6 +434,8 @@ class AuditLogAction(Enum):
return 'integration'
elif v < 90:
return 'stage_instance'
+ elif v < 93:
+ return 'sticker'
elif v < 113:
return 'thread'
@@ -484,10 +493,26 @@ ExpireBehavior = ExpireBehaviour
class StickerType(Enum):
+ standard = 1
+ guild = 2
+
+
+class StickerFormatType(Enum):
png = 1
apng = 2
lottie = 3
+ @property
+ def file_extension(self) -> str:
+ # fmt: off
+ lookup: Dict[StickerFormatType, str] = {
+ StickerFormatType.png: 'png',
+ StickerFormatType.apng: 'png',
+ StickerFormatType.lottie: 'json',
+ }
+ # fmt: on
+ return lookup[self]
+
class InviteTarget(Enum):
unknown = 0
diff --git a/discord/flags.py b/discord/flags.py
index 373a8957..1a3ca792 100644
--- a/discord/flags.py
+++ b/discord/flags.py
@@ -566,18 +566,34 @@ class Intents(BaseFlags):
@flag_value
def emojis(self):
- """:class:`bool`: Whether guild emoji related events are enabled.
+ """:class:`bool`: Alias of :attr:`.emojis_and_stickers`.
+
+ .. versionchanged:: 2.0
+ Changed to an alias.
+ """
+ return 1 << 3
+
+ @alias_flag_value
+ def emojis_and_stickers(self):
+ """:class:`bool`: Whether guild emoji and sticker related events are enabled.
+
+ .. versionadded:: 2.0
This corresponds to the following events:
- :func:`on_guild_emojis_update`
+ - :func:`on_guild_stickers_update`
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Emoji`
+ - :class:`GuildSticker`
- :meth:`Client.get_emoji`
+ - :meth:`Client.get_sticker`
- :meth:`Client.emojis`
+ - :meth:`Client.stickers`
- :attr:`Guild.emojis`
+ - :attr:`Guild.stickers`
"""
return 1 << 3
diff --git a/discord/guild.py b/discord/guild.py
index c65296db..c61ed7f0 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import copy
+import unicodedata
from typing import (
Any,
ClassVar,
@@ -72,6 +73,9 @@ from .flags import SystemChannelFlags
from .integrations import Integration, _integration_factory
from .stage_instance import StageInstance
from .threads import Thread
+from .sticker import GuildSticker
+from .file import File
+
__all__ = (
'Guild',
@@ -107,6 +111,7 @@ class BanEntry(NamedTuple):
class _GuildLimit(NamedTuple):
emoji: int
+ stickers: int
bitrate: float
filesize: int
@@ -140,6 +145,10 @@ class Guild(Hashable):
The guild name.
emojis: Tuple[:class:`Emoji`, ...]
All emojis that the guild owns.
+ stickers: Tuple[:class:`GuildSticker`, ...]
+ All stickers that the guild owns.
+
+ .. versionadded:: 2.0
region: :class:`VoiceRegion`
The region the guild belongs on. There is a chance that the region
will be a :class:`str` if the value is not recognised by the enumerator.
@@ -234,6 +243,7 @@ class Guild(Hashable):
'owner_id',
'mfa_level',
'emojis',
+ 'stickers',
'features',
'verification_level',
'explicit_content_filter',
@@ -266,11 +276,11 @@ class Guild(Hashable):
)
_PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = {
- None: _GuildLimit(emoji=50, bitrate=96e3, filesize=8388608),
- 0: _GuildLimit(emoji=50, bitrate=96e3, filesize=8388608),
- 1: _GuildLimit(emoji=100, bitrate=128e3, filesize=8388608),
- 2: _GuildLimit(emoji=150, bitrate=256e3, filesize=52428800),
- 3: _GuildLimit(emoji=250, bitrate=384e3, filesize=104857600),
+ None: _GuildLimit(emoji=50, stickers=0, bitrate=96e3, filesize=8388608),
+ 0: _GuildLimit(emoji=50, stickers=0, bitrate=96e3, filesize=8388608),
+ 1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=8388608),
+ 2: _GuildLimit(emoji=150, stickers=30, bitrate=256e3, filesize=52428800),
+ 3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600),
}
def __init__(self, *, data: GuildPayload, state: ConnectionState):
@@ -412,6 +422,7 @@ class Guild(Hashable):
self.mfa_level: MFALevel = guild.get('mfa_level')
self.emojis: Tuple[Emoji, ...] = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', [])))
+ self.stickers: Tuple[GuildSticker, ...] = tuple(map(lambda d: state.store_sticker(self, d), guild.get('stickers', [])))
self.features: List[GuildFeature] = guild.get('features', [])
self._splash: Optional[str] = guild.get('splash')
self._system_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'system_channel_id')
@@ -699,6 +710,15 @@ class Guild(Hashable):
return max(more_emoji, self._PREMIUM_GUILD_LIMITS[self.premium_tier].emoji)
@property
+ def sticker_limit(self) -> int:
+ """:class:`int`: The maximum number of sticker slots this guild has.
+
+ .. versionadded:: 2.0
+ """
+ more_stickers = 60 if 'MORE_STICKERS' in self.features else 15
+ return max(more_stickers, self._PREMIUM_GUILD_LIMITS[self.premium_tier].stickers)
+
+ @property
def bitrate_limit(self) -> float:
""":class:`float`: The maximum bitrate for voice channels this guild can have."""
vip_guild = self._PREMIUM_GUILD_LIMITS[1].bitrate if 'VIP_REGIONS' in self.features else 96e3
@@ -2027,6 +2047,150 @@ class Guild(Hashable):
return [convert(d) for d in data]
+ async def fetch_stickers(self) -> List[GuildSticker]:
+ r"""|coro|
+
+ Retrieves a list of all :class:`Sticker`\s for the guild.
+
+ .. versionadded:: 2.0
+
+ .. note::
+
+ This method is an API call. For general usage, consider :attr:`stickers` instead.
+
+ Raises
+ ---------
+ HTTPException
+ An error occurred fetching the stickers.
+
+ Returns
+ --------
+ List[:class:`GuildSticker`]
+ The retrieved stickers.
+ """
+ data = await self._state.http.get_all_guild_stickers(self.id)
+ return [GuildSticker(state=self._state, data=d) for d in data]
+
+ async def fetch_sticker(self, sticker_id: int, /) -> GuildSticker:
+ """|coro|
+
+ Retrieves a custom :class:`Sticker` from the guild.
+
+ .. versionadded:: 2.0
+
+ .. note::
+
+ This method is an API call.
+ For general usage, consider iterating over :attr:`stickers` instead.
+
+ Parameters
+ -------------
+ sticker_id: :class:`int`
+ The sticker's ID.
+
+ Raises
+ ---------
+ NotFound
+ The sticker requested could not be found.
+ HTTPException
+ An error occurred fetching the sticker.
+
+ Returns
+ --------
+ :class:`GuildSticker`
+ The retrieved sticker.
+ """
+ data = await self._state.http.get_guild_sticker(self.id, sticker_id)
+ return GuildSticker(state=self._state, data=data)
+
+ async def create_sticker(
+ self,
+ *,
+ name: str,
+ description: Optional[str] = None,
+ emoji: str,
+ file: File,
+ reason: Optional[str] = None,
+ ) -> GuildSticker:
+ """|coro|
+
+ Creates a :class:`Sticker` for the guild.
+
+ You must have :attr:`~Permissions.manage_emojis_and_stickers` permission to
+ do this.
+
+ .. versionadded:: 2.0
+
+ Parameters
+ -----------
+ name: :class:`str`
+ The sticker name. Must be at least 2 characters.
+ description: Optional[:class:`str`]
+ The sticker's description. Can be ``None``.
+ emoji: :class:`str`
+ The name of a unicode emoji that represents the sticker's expression.
+ file: :class:`File`
+ The file of the sticker to upload.
+ reason: :class:`str`
+ The reason for creating this sticker. Shows up on the audit log.
+
+ Raises
+ -------
+ Forbidden
+ You are not allowed to create stickers.
+ HTTPException
+ An error occurred creating a sticker.
+
+ Returns
+ --------
+ :class:`GuildSticker`
+ The created sticker.
+ """
+ payload = {
+ 'name': name,
+ }
+
+ if description:
+ payload['description'] = description
+
+ try:
+ emoji = unicodedata.name(emoji)
+ except TypeError:
+ pass
+ else:
+ emoji = emoji.replace(' ', '_')
+
+ payload['tags'] = emoji
+
+ data = await self._state.http.create_guild_sticker(self.id, payload, file, reason)
+ return self._state.store_sticker(self, data)
+
+ async def delete_sticker(self, sticker: Snowflake, *, reason: Optional[str] = None) -> None:
+ """|coro|
+
+ Deletes the custom :class:`Sticker` from the guild.
+
+ You must have :attr:`~Permissions.manage_emojis_and_stickers` permission to
+ do this.
+
+ .. versionadded:: 2.0
+
+ Parameters
+ -----------
+ sticker: :class:`abc.Snowflake`
+ The sticker you are deleting.
+ reason: Optional[:class:`str`]
+ The reason for deleting this sticker. Shows up on the audit log.
+
+ Raises
+ -------
+ Forbidden
+ You are not allowed to delete stickers.
+ HTTPException
+ An error occurred deleting the sticker.
+ """
+ await self._state.http.delete_guild_sticker(self.id, sticker.id, reason)
+
async def fetch_emojis(self) -> List[Emoji]:
r"""|coro|
diff --git a/discord/http.py b/discord/http.py
index fd0f1e78..7c18b701 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -49,7 +49,7 @@ import weakref
import aiohttp
-from .errors import HTTPException, Forbidden, NotFound, LoginFailure, DiscordServerError, GatewayNotFound
+from .errors import HTTPException, Forbidden, NotFound, LoginFailure, DiscordServerError, GatewayNotFound, InvalidArgument
from .gateway import DiscordClientWebSocketResponse
from . import __version__, utils
from .utils import MISSING
@@ -84,6 +84,7 @@ if TYPE_CHECKING:
widget,
threads,
voice,
+ sticker,
)
from .types.snowflake import Snowflake, SnowflakeList
@@ -420,9 +421,10 @@ class HTTPClient:
tts: bool = False,
embed: Optional[embed.Embed] = None,
embeds: Optional[List[embed.Embed]] = None,
- nonce: Optional[str] = None,
+ nonce: Optional[str] = None,
allowed_mentions: Optional[message.AllowedMentions] = None,
message_reference: Optional[message.MessageReference] = None,
+ stickers: Optional[List[sticker.StickerItem]] = None,
components: Optional[List[components.Component]] = None,
) -> Response[message.Message]:
r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id)
@@ -452,6 +454,9 @@ class HTTPClient:
if components:
payload['components'] = components
+ if stickers:
+ payload['sticker_items'] = stickers
+
return self.request(r, json=payload)
def send_typing(self, channel_id: Snowflake) -> Response[None]:
@@ -465,10 +470,11 @@ class HTTPClient:
content: Optional[str] = None,
tts: bool = False,
embed: Optional[embed.Embed] = None,
- embeds: Iterable[Optional[embed.Embed]] = None,
+ embeds: Optional[Iterable[Optional[embed.Embed]]] = None,
nonce: Optional[str] = None,
allowed_mentions: Optional[message.AllowedMentions] = None,
message_reference: Optional[message.MessageReference] = None,
+ stickers: Optional[List[sticker.StickerItem]] = None,
components: Optional[List[components.Component]] = None,
) -> Response[message.Message]:
form = []
@@ -488,6 +494,8 @@ class HTTPClient:
payload['message_reference'] = message_reference
if components:
payload['components'] = components
+ if stickers:
+ payload['sticker_items'] = stickers
form.append({'name': 'payload_json', 'value': utils.to_json(payload)})
if len(files) == 1:
@@ -525,6 +533,7 @@ class HTTPClient:
nonce: Optional[str] = None,
allowed_mentions: Optional[message.AllowedMentions] = None,
message_reference: Optional[message.MessageReference] = None,
+ stickers: Optional[List[sticker.StickerItem]] = None,
components: Optional[List[components.Component]] = None,
) -> Response[message.Message]:
r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id)
@@ -538,6 +547,7 @@ class HTTPClient:
nonce=nonce,
allowed_mentions=allowed_mentions,
message_reference=message_reference,
+ stickers=stickers,
components=components,
)
@@ -1160,6 +1170,54 @@ class HTTPClient:
return self.request(Route('GET', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params)
+ def get_sticker(self, sticker_id: Snowflake) -> Response[sticker.Sticker]:
+ return self.request(Route('GET', '/stickers/{sticker_id}', sticker_id=sticker_id))
+
+ def list_nitro_sticker_packs(self) -> Response[sticker.ListNitroStickerPacks]:
+ return self.request(Route('GET', '/sticker-packs'))
+
+ def get_all_guild_stickers(self, guild_id: Snowflake) -> Response[List[sticker.GuildSticker]]:
+ return self.request(Route('GET', '/guilds/{guild_id}/stickers', guild_id=guild_id))
+
+ def get_guild_sticker(self, guild_id: Snowflake, sticker_id: Snowflake) -> Response[sticker.GuildSticker]:
+ return self.request(Route('GET', '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id))
+
+ def create_guild_sticker(self, guild_id: Snowflake, payload: sticker.CreateGuildSticker, file: File, reason: str) -> Response[sticker.GuildSticker]:
+ initial_bytes = file.fp.read(16)
+
+ try:
+ mime_type = utils._get_mime_type_for_image(initial_bytes)
+ except InvalidArgument:
+ if initial_bytes.startswith(b'{'):
+ mime_type = 'application/json'
+ else:
+ mime_type = 'application/octet-stream'
+ finally:
+ file.reset()
+
+ form: List[Dict[str, Any]] = [
+ {
+ 'name': 'file',
+ 'value': file.fp,
+ 'filename': file.filename,
+ 'content_type': mime_type,
+ }
+ ]
+
+ for k, v in payload.items():
+ form.append({
+ 'name': k,
+ 'value': v,
+ })
+
+ return self.request(Route('POST', '/guilds/{guild_id}/stickers', guild_id=guild_id), form=form, files=[file], reason=reason)
+
+ def modify_guild_sticker(self, guild_id: Snowflake, sticker_id: Snowflake, payload: sticker.EditGuildSticker, reason: str) -> Response[sticker.GuildSticker]:
+ return self.request(Route('PATCH', '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id), json=payload, reason=reason)
+
+ def delete_guild_sticker(self, guild_id: Snowflake, sticker_id: Snowflake, reason: str) -> Response[None]:
+ return self.request(Route('DELETE', '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id), reason=reason)
+
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))
diff --git a/discord/message.py b/discord/message.py
index 82f812db..270ac9e9 100644
--- a/discord/message.py
+++ b/discord/message.py
@@ -45,7 +45,7 @@ from .file import File
from .utils import escape_mentions, MISSING
from .guild import Guild
from .mixins import Hashable
-from .sticker import Sticker
+from .sticker import StickerItem
from .threads import Thread
if TYPE_CHECKING:
@@ -588,8 +588,8 @@ class Message(Hashable):
- ``description``: A string representing the application's description.
- ``icon``: A string representing the icon ID of the application.
- ``cover_image``: A string representing the embed's image asset ID.
- stickers: List[:class:`Sticker`]
- A list of stickers given to the message.
+ stickers: List[:class:`StickerItem`]
+ A list of sticker items given to the message.
.. versionadded:: 1.6
components: List[:class:`Component`]
@@ -666,7 +666,7 @@ class Message(Hashable):
self.tts: bool = data['tts']
self.content: str = data['content']
self.nonce: Optional[Union[int, str]] = data.get('nonce')
- self.stickers: List[Sticker] = [Sticker(data=d, state=state) for d in data.get('stickers', [])]
+ self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
self.components: List[Component] = [_component_factory(d) for d in data.get('components', [])]
try:
diff --git a/discord/permissions.py b/discord/permissions.py
index ab7ecbfe..04c475fa 100644
--- a/discord/permissions.py
+++ b/discord/permissions.py
@@ -462,6 +462,14 @@ class Permissions(BaseFlags):
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis."""
return 1 << 30
+ @make_permission_alias('manage_emojis')
+ def manage_emojis_and_stickers(self):
+ """:class:`bool`: An alias for :attr:`manage_emojis`.
+
+ .. versionadded:: 2.0
+ """
+ return 1 << 30
+
@flag_value
def use_slash_commands(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use slash commands.
@@ -510,6 +518,22 @@ class Permissions(BaseFlags):
"""
return 1 << 36
+ @flag_value
+ def external_stickers(self) -> int:
+ """:class:`bool`: Returns ``True`` if a user can use stickers from other guilds.
+
+ .. versionadded:: 2.0
+ """
+ return 1 << 37
+
+ @make_permission_alias('external_stickers')
+ def use_external_stickers(self) -> int:
+ """:class:`bool`: An alias for :attr:`external_stickers`.
+
+ .. versionadded:: 2.0
+ """
+ return 1 << 37
+
PO = TypeVar('PO', bound='PermissionOverwrite')
def _augment_from_permissions(cls):
diff --git a/discord/state.py b/discord/state.py
index fd0587bf..ec8ce94c 100644
--- a/discord/state.py
+++ b/discord/state.py
@@ -57,6 +57,7 @@ from .interactions import Interaction
from .ui.view import ViewStore
from .stage_instance import StageInstance
from .threads import Thread, ThreadMember
+from .sticker import GuildSticker
class ChunkRequest:
def __init__(self, guild_id, loop, resolver, *, cache=True):
@@ -204,6 +205,7 @@ class ConnectionState:
# though more testing will have to be done.
self._users: Dict[int, User] = {}
self._emojis = {}
+ self._stickers = {}
self._guilds = {}
self._view_store = ViewStore(self)
self._voice_clients = {}
@@ -298,6 +300,11 @@ class ConnectionState:
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
return emoji
+ def store_sticker(self, guild, data):
+ sticker_id = int(data['id'])
+ self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
+ return sticker
+
def store_view(self, view, message_id=None):
self._view_store.add_view(view, message_id)
@@ -324,15 +331,25 @@ class ConnectionState:
for emoji in guild.emojis:
self._emojis.pop(emoji.id, None)
+ for sticker in guild.stickers:
+ self._stickers.pop(sticker.id, None)
+
del guild
@property
def emojis(self):
return list(self._emojis.values())
+ @property
+ def stickers(self):
+ return list(self._stickers.values())
+
def get_emoji(self, emoji_id):
return self._emojis.get(emoji_id)
+ def get_sticker(self, sticker_id):
+ return self._stickers.get(sticker_id)
+
@property
def private_channels(self):
return list(self._private_channels.values())
@@ -925,6 +942,18 @@ class ConnectionState:
guild.emojis = tuple(map(lambda d: self.store_emoji(guild, d), data['emojis']))
self.dispatch('guild_emojis_update', guild, before_emojis, guild.emojis)
+ def parse_guild_stickers_update(self, data):
+ guild = self._get_guild(int(data['guild_id']))
+ if guild is None:
+ log.debug('GUILD_STICKERS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
+ return
+
+ before_stickers = guild.stickers
+ for emoji in before_stickers:
+ self._stickers.pop(emoji.id, None)
+ guild.stickers = tuple(map(lambda d: self.store_sticker(guild, d), data['stickers']))
+ self.dispatch('guild_stickers_update', guild, before_stickers, guild.stickers)
+
def _get_create_guild(self, data):
if data.get('unavailable') is False:
# GUILD_CREATE with unavailable in the response
diff --git a/discord/sticker.py b/discord/sticker.py
index 235b66f2..b93f5baf 100644
--- a/discord/sticker.py
+++ b/discord/sticker.py
@@ -23,24 +23,213 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
-from typing import TYPE_CHECKING, List, Optional
+from typing import Literal, TYPE_CHECKING, List, Optional, Tuple, Type, Union
+import unicodedata
from .mixins import Hashable
-from .asset import Asset
-from .utils import snowflake_time
-from .enums import StickerType, try_enum
+from .asset import Asset, AssetMixin
+from .utils import cached_slot_property, find, snowflake_time, get, MISSING
+from .errors import InvalidData
+from .enums import StickerType, StickerFormatType, try_enum
__all__ = (
+ 'StickerPack',
+ 'StickerItem',
'Sticker',
+ 'StandardSticker',
+ 'GuildSticker',
)
if TYPE_CHECKING:
import datetime
from .state import ConnectionState
- from .types.message import Sticker as StickerPayload
+ from .user import User
+ from .guild import Guild
+ from .types.sticker import (
+ StickerPack as StickerPackPayload,
+ StickerItem as StickerItemPayload,
+ Sticker as StickerPayload,
+ StandardSticker as StandardStickerPayload,
+ GuildSticker as GuildStickerPayload,
+ ListNitroStickerPacks as ListNitroStickerPacksPayload
+ )
-class Sticker(Hashable):
+class StickerPack(Hashable):
+ """Represents a sticker pack.
+
+ .. versionadded:: 2.0
+
+ .. container:: operations
+
+ .. describe:: str(x)
+
+ Returns the name of the sticker pack.
+
+ .. describe:: x == y
+
+ Checks if the sticker pack is equal to another sticker pack.
+
+ .. describe:: x != y
+
+ Checks if the sticker pack is not equal to another sticker pack.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The name of the sticker pack.
+ description: :class:`str`
+ The description of the sticker pack.
+ id: :class:`int`
+ The id of the sticker pack.
+ stickers: List[:class:`StandardSticker`]
+ The stickers of this sticker pack.
+ sku_id: :class:`int`
+ The SKU ID of the sticker pack.
+ cover_sticker_id: :class:`int`
+ The ID of the sticker used for the cover of the sticker pack.
+ cover_sticker: :class:`StandardSticker`
+ The sticker used for the cover of the sticker pack.
+ """
+
+ __slots__ = (
+ '_state',
+ 'id',
+ 'stickers',
+ 'name',
+ 'sku_id',
+ 'cover_sticker_id',
+ 'cover_sticker',
+ 'description',
+ '_banner',
+ )
+
+ def __init__(self, *, state: ConnectionState, data: StickerPackPayload) -> None:
+ self._state: ConnectionState = state
+ self._from_data(data)
+
+ def _from_data(self, data: StickerPackPayload) -> None:
+ self.id: int = int(data['id'])
+ stickers = data['stickers']
+ self.stickers: List[StandardSticker] = [StandardSticker(state=self._state, data=sticker) for sticker in stickers]
+ self.name: str = data['name']
+ self.sku_id: int = int(data['sku_id'])
+ self.cover_sticker_id: int = int(data['cover_sticker_id'])
+ self.cover_sticker: StandardSticker = get(self.stickers, id=self.cover_sticker_id) # type: ignore
+ self.description: str = data['description']
+ self._banner: int = int(data['banner_asset_id'])
+
+ @property
+ def banner(self) -> Asset:
+ """:class:`Asset`: The banner asset of the sticker pack."""
+ return Asset._from_sticker_banner(self._state, self._banner)
+
+ def __repr__(self) -> str:
+ return f'<StickerPack id={self.id} name={self.name!r} description={self.description!r}>'
+
+ def __str__(self) -> str:
+ return self.name
+
+
+class _StickerTag(Hashable, AssetMixin):
+ __slots__ = ()
+
+ id: int
+ format: StickerFormatType
+
+ async def read(self) -> bytes:
+ """|coro|
+
+ Retrieves the content of this sticker as a :class:`bytes` object.
+
+ .. note::
+
+ Stickers that use the :attr:`StickerFormatType.lottie` format cannot be read.
+
+ Raises
+ ------
+ HTTPException
+ Downloading the asset failed.
+ NotFound
+ The asset was deleted.
+
+ Returns
+ -------
+ :class:`bytes`
+ The content of the asset.
+ """
+ if self.format is StickerFormatType.lottie:
+ raise TypeError('Cannot read stickers of format "lottie".')
+ return await super().read()
+
+
+class StickerItem(_StickerTag):
+ """Represents a sticker item.
+
+ .. versionadded:: 2.0
+
+ .. container:: operations
+
+ .. describe:: str(x)
+
+ Returns the name of the sticker item.
+
+ .. describe:: x == y
+
+ Checks if the sticker item is equal to another sticker item.
+
+ .. describe:: x != y
+
+ Checks if the sticker item is not equal to another sticker item.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The sticker's name.
+ id: :class:`int`
+ The id of the sticker.
+ format: :class:`StickerFormatType`
+ The format for the sticker's image.
+ url: :class:`str`
+ The URL for the sticker's image.
+ """
+
+ __slots__ = ('_state', 'name', 'id', 'format', 'url')
+
+ def __init__(self, *, state: ConnectionState, data: StickerItemPayload):
+ self._state: ConnectionState = state
+ self.name: str = data['name']
+ self.id: int = int(data['id'])
+ self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type'])
+ self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}'
+
+ def __repr__(self) -> str:
+ return f'<StickerItem id={self.id} name={self.name!r} format={self.format}>'
+
+ def __str__(self) -> str:
+ return self.name
+
+ async def fetch(self) -> Union[Sticker, StandardSticker, GuildSticker]:
+ """|coro|
+
+ Attempts to retrieve the full sticker data of the sticker item.
+
+ Raises
+ --------
+ HTTPException
+ Retrieving the sticker failed.
+
+ Returns
+ --------
+ Union[:class:`StandardSticker`, :class:`GuildSticker`]
+ The retrieved sticker.
+ """
+ data: StickerPayload = await self._state.http.get_sticker(self.id)
+ cls, _ = _sticker_factory(data['type']) # type: ignore
+ return cls(state=self._state, data=data)
+
+
+class Sticker(_StickerTag):
"""Represents a sticker.
.. versionadded:: 1.6
@@ -69,30 +258,27 @@ class Sticker(Hashable):
The description of the sticker.
pack_id: :class:`int`
The id of the sticker's pack.
- format: :class:`StickerType`
+ format: :class:`StickerFormatType`
The format for the sticker's image.
- tags: List[:class:`str`]
- A list of tags for the sticker.
+ url: :class:`str`
+ The URL for the sticker's image.
"""
- __slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', '_image', 'tags')
+ __slots__ = ('_state', 'id', 'name', 'description', 'format', 'url')
- def __init__(self, *, state: ConnectionState, data: StickerPayload):
+ def __init__(self, *, state: ConnectionState, data: StickerPayload) -> None:
self._state: ConnectionState = state
+ self._from_data(data)
+
+ def _from_data(self, data: StickerPayload) -> None:
self.id: int = int(data['id'])
self.name: str = data['name']
self.description: str = data['description']
- self.pack_id: int = int(data.get('pack_id', 0))
- self.format: StickerType = try_enum(StickerType, data['format_type'])
- self._image: str = data['asset']
-
- try:
- self.tags: List[str] = [tag.strip() for tag in data['tags'].split(',')]
- except KeyError:
- self.tags = []
+ self.format: StickerFormatType = try_enum(StickerFormatType, data['format_type'])
+ self.url: str = f'{Asset.BASE}/stickers/{self.id}.{self.format.file_extension}'
def __repr__(self) -> str:
- return f'<{self.__class__.__name__} id={self.id} name={self.name!r}>'
+ return f'<Sticker id={self.id} name={self.name!r}>'
def __str__(self) -> str:
return self.name
@@ -102,19 +288,229 @@ class Sticker(Hashable):
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
return snowflake_time(self.id)
- @property
- def image(self) -> Optional[Asset]:
- """Returns an :class:`Asset` for the sticker's image.
- .. note::
- This will return ``None`` if the format is ``StickerType.lottie``.
+class StandardSticker(Sticker):
+ """Represents a sticker that is found in a standard sticker pack.
+
+ .. versionadded:: 2.0
+
+ .. container:: operations
+
+ .. describe:: str(x)
+
+ Returns the name of the sticker.
+
+ .. describe:: x == y
+
+ Checks if the sticker is equal to another sticker.
+
+ .. describe:: x != y
+
+ Checks if the sticker is not equal to another sticker.
+
+ Attributes
+ ----------
+ name: :class:`str`
+ The sticker's name.
+ id: :class:`int`
+ The id of the sticker.
+ description: :class:`str`
+ The description of the sticker.
+ pack_id: :class:`int`
+ The id of the sticker's pack.
+ format: :class:`StickerFormatType`
+ The format for the sticker's image.
+ tags: List[:class:`str`]
+ A list of tags for the sticker.
+ sort_value: :class:`int`
+ The sticker's sort order within its pack.
+ """
+
+ __slots__ = ('sort_value', 'pack_id', 'type', 'tags')
+
+ def _from_data(self, data: StandardStickerPayload) -> None:
+ super()._from_data(data)
+ self.sort_value: int = data['sort_value']
+ self.pack_id: int = int(data['pack_id'])
+ self.type: StickerType = StickerType.standard
+
+ try:
+ self.tags: List[str] = [tag.strip() for tag in data['tags'].split(',')]
+ except KeyError:
+ self.tags = []
+
+ def __repr__(self) -> str:
+ return f'<StandardSticker id={self.id} name={self.name!r} pack_id={self.pack_id}>'
+
+ async def pack(self) -> StickerPack:
+ """|coro|
+
+ Retrieves the sticker pack that this sticker belongs to.
+
+ Raises
+ --------
+ InvalidData
+ The corresponding sticker pack was not found.
+ HTTPException
+ Retrieving the sticker pack failed.
Returns
+ --------
+ :class:`StickerPack`
+ The retrieved sticker pack.
+ """
+ data: ListNitroStickerPacksPayload = await self._state.http.list_nitro_sticker_packs()
+ packs = data['sticker_packs']
+ pack = find(lambda d: int(d['id']) == self.pack_id, packs)
+
+ if pack:
+ return StickerPack(state=self._state, data=pack)
+ raise InvalidData(f'Could not find corresponding sticker pack for {self!r}')
+
+
+class GuildSticker(Sticker):
+ """Represents a sticker that belongs to a guild.
+
+ .. versionadded:: 2.0
+
+ .. container:: operations
+
+ .. describe:: str(x)
+
+ Returns the name of the sticker.
+
+ .. describe:: x == y
+
+ Checks if the sticker is equal to another sticker.
+
+ .. describe:: x != y
+
+ Checks if the sticker is not equal to another sticker.
+
+ Attributes
+ ----------
+ name: :class:`str`
+ The sticker's name.
+ id: :class:`int`
+ The id of the sticker.
+ description: :class:`str`
+ The description of the sticker.
+ format: :class:`StickerFormatType`
+ The format for the sticker's image.
+ available: :class:`bool`
+ Whether this sticker is available for use.
+ guild_id: :class:`int`
+ The ID of the guild that this sticker is from.
+ user: Optional[:class:`User`]
+ The user that created this sticker. This can only be retrieved using :meth:`Guild.fetch_sticker` and
+ having the :attr:`~Permissions.manage_emojis_and_stickers` permission.
+ emoji: :class:`str`
+ The name of a unicode emoji that represents this sticker.
+ """
+
+ __slots__ = ('available', 'guild_id', 'user', 'emoji', 'type', '_cs_guild')
+
+ def _from_data(self, data: GuildStickerPayload) -> None:
+ super()._from_data(data)
+ self.available: bool = data['available']
+ self.guild_id: int = int(data['guild_id'])
+ user = data.get('user')
+ self.user: Optional[User] = self._state.store_user(user) if user else None
+ self.emoji: str = data['tags']
+ self.type: StickerType = StickerType.guild
+
+ def __repr__(self) -> str:
+ return f'<GuildSticker name={self.name!r} id={self.id} guild_id={self.guild_id} user={self.user!r}>'
+
+ @cached_slot_property('_cs_guild')
+ def guild(self) -> Optional[Guild]:
+ """Optional[:class:`Guild`]: The guild that this sticker is from.
+ Could be ``None`` if the bot is not in the guild.
+
+ .. versionadded:: 2.0
+ """
+ return self._state._get_guild(self.guild_id)
+
+ async def edit(
+ self,
+ *,
+ name: str = MISSING,
+ description: str = MISSING,
+ emoji: str = MISSING,
+ reason: Optional[str] = None,
+ ) -> None:
+ """|coro|
+
+ Edits a :class:`Sticker` for the guild.
+
+ Parameters
+ -----------
+ name: :class:`str`
+ The sticker's new name. Must be at least 2 characters.
+ description: Optional[:class:`str`]
+ The sticker's new description. Can be ``None``.
+ emoji: :class:`str`
+ The name of a unicode emoji that represents the sticker's expression.
+ reason: :class:`str`
+ The reason for editing this sticker. Shows up on the audit log.
+
+ Raises
+ -------
+ Forbidden
+ You are not allowed to edit stickers.
+ HTTPException
+ An error occurred editing the sticker.
+ """
+ payload = {}
+
+ if name is not MISSING:
+ payload['name'] = name
+
+ if description is not MISSING:
+ payload['description'] = description
+
+ if emoji is not MISSING:
+ try:
+ emoji = unicodedata.name(emoji)
+ except TypeError:
+ pass
+ else:
+ emoji = emoji.replace(' ', '_')
+
+ payload['tags'] = emoji
+
+ data: GuildStickerPayload = await self._state.http.modify_guild_sticker(self.guild_id, self.id, payload, reason)
+
+ self._from_data(data)
+
+ async def delete(self, *, reason: Optional[str] = None) -> None:
+ """|coro|
+
+ Deletes the custom :class:`Sticker` from the guild.
+
+ You must have :attr:`~Permissions.manage_emojis_and_stickers` permission to
+ do this.
+
+ Parameters
+ -----------
+ reason: Optional[:class:`str`]
+ The reason for deleting this sticker. Shows up on the audit log.
+
+ Raises
-------
- Optional[:class:`Asset`]
- The resulting CDN asset.
+ Forbidden
+ You are not allowed to delete stickers.
+ HTTPException
+ An error occurred deleting the sticker.
"""
- if self.format is StickerType.lottie:
- return None
+ await self._state.http.delete_guild_sticker(self.guild_id, self.id, reason)
+
- return Asset._from_sticker(self._state, self.id, self._image)
+def _sticker_factory(sticker_type: Literal[1, 2]) -> Tuple[Type[Union[StandardSticker, GuildSticker, Sticker]], StickerType]:
+ value = try_enum(StickerType, sticker_type)
+ if value == StickerType.standard:
+ return StandardSticker, value
+ elif value == StickerType.guild:
+ return GuildSticker, value
+ else:
+ return Sticker, value
diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py
index e784e7df..563819fd 100644
--- a/discord/types/audit_log.py
+++ b/discord/types/audit_log.py
@@ -73,6 +73,9 @@ AuditLogEvent = Literal[
83,
84,
85,
+ 90,
+ 91,
+ 92,
110,
111,
112,
@@ -81,14 +84,14 @@ AuditLogEvent = Literal[
class _AuditLogChange_Str(TypedDict):
key: Literal[
- 'name', 'description', 'preferred_locale', 'vanity_url_code', 'topic', 'code', 'allow', 'deny', 'permissions'
+ 'name', 'description', 'preferred_locale', 'vanity_url_code', 'topic', 'code', 'allow', 'deny', 'permissions', 'tags'
]
new_value: str
old_value: str
class _AuditLogChange_AssetHash(TypedDict):
- key: Literal['icon_hash', 'splash_hash', 'discovery_splash_hash', 'banner_hash', 'avatar_hash']
+ key: Literal['icon_hash', 'splash_hash', 'discovery_splash_hash', 'banner_hash', 'avatar_hash', 'asset']
new_value: str
old_value: str
@@ -105,6 +108,7 @@ class _AuditLogChange_Snowflake(TypedDict):
'application_id',
'channel_id',
'inviter_id',
+ 'guild_id',
]
new_value: Snowflake
old_value: Snowflake
@@ -123,6 +127,7 @@ class _AuditLogChange_Bool(TypedDict):
'enabled_emoticons',
'region',
'rtc_region',
+ 'available',
'archived',
'locked',
]
diff --git a/discord/types/message.py b/discord/types/message.py
index 2dbdf983..5448a56a 100644
--- a/discord/types/message.py
+++ b/discord/types/message.py
@@ -33,6 +33,7 @@ from .embed import Embed
from .channel import ChannelType
from .components import Component
from .interactions import MessageInteraction
+from .sticker import StickerItem
class ChannelMention(TypedDict):
@@ -89,22 +90,6 @@ class MessageReference(TypedDict, total=False):
fail_if_not_exists: bool
-class _StickerOptional(TypedDict, total=False):
- tags: str
-
-
-StickerFormatType = Literal[1, 2, 3]
-
-
-class Sticker(_StickerOptional):
- id: Snowflake
- pack_id: Snowflake
- name: str
- description: str
- asset: str
- format_type: StickerFormatType
-
-
class _MessageOptional(TypedDict, total=False):
guild_id: Snowflake
member: Member
@@ -117,7 +102,7 @@ class _MessageOptional(TypedDict, total=False):
application_id: Snowflake
message_reference: MessageReference
flags: int
- stickers: List[Sticker]
+ sticker_items: List[StickerItem]
referenced_message: Optional[Message]
interaction: MessageInteraction
components: List[Component]
diff --git a/discord/types/sticker.py b/discord/types/sticker.py
new file mode 100644
index 00000000..11150593
--- /dev/null
+++ b/discord/types/sticker.py
@@ -0,0 +1,93 @@
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-present Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+from __future__ import annotations
+
+from typing import List, Literal, TypedDict, Union
+from .snowflake import Snowflake
+from .user import User
+
+StickerFormatType = Literal[1, 2, 3]
+
+
+class StickerItem(TypedDict):
+ id: Snowflake
+ name: str
+ format_type: StickerFormatType
+
+
+class BaseSticker(TypedDict):
+ id: Snowflake
+ name: str
+ description: str
+ tags: str
+ format_type: StickerFormatType
+
+
+class StandardSticker(BaseSticker):
+ type: Literal[1]
+ sort_value: int
+ pack_id: Snowflake
+
+
+class _GuildStickerOptional(TypedDict, total=False):
+ user: User
+
+
+class GuildSticker(BaseSticker, _GuildStickerOptional):
+ type: Literal[2]
+ available: bool
+ guild_id: Snowflake
+
+
+Sticker = Union[BaseSticker, StandardSticker, GuildSticker]
+
+
+class StickerPack(TypedDict):
+ id: Snowflake
+ stickers: List[StandardSticker]
+ name: str
+ sku_id: Snowflake
+ cover_sticker_id: Snowflake
+ description: str
+ banner_asset_id: Snowflake
+
+
+class _CreateGuildStickerOptional(TypedDict, total=False):
+ description: str
+
+
+class CreateGuildSticker(_CreateGuildStickerOptional):
+ name: str
+ tags: str
+
+
+class EditGuildSticker(TypedDict, total=False):
+ name: str
+ tags: str
+ description: str
+
+
+class ListNitroStickerPacks(TypedDict):
+ sticker_packs: List[StickerPack]
diff --git a/docs/api.rst b/docs/api.rst
index 2f4d911b..1c50190b 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -926,7 +926,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
Called when a :class:`Guild` adds or removes :class:`Emoji`.
- This requires :attr:`Intents.emojis` to be enabled.
+ This requires :attr:`Intents.emojis_and_stickers` to be enabled.
:param guild: The guild who got their emojis updated.
:type guild: :class:`Guild`
@@ -935,6 +935,21 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param after: A list of emojis after the update.
:type after: Sequence[:class:`Emoji`]
+.. function:: on_guild_stickers_update(guild, before, after)
+
+ Called when a :class:`Guild` updates its stickers.
+
+ This requires :attr:`Intents.emojis_and_stickers` to be enabled.
+
+ .. versionadded:: 2.0
+
+ :param guild: The guild who got their stickers updated.
+ :type guild: :class:`Guild`
+ :param before: A list of stickers before the update.
+ :type before: Sequence[:class:`GuildSticker`]
+ :param after: A list of stickers after the update.
+ :type after: Sequence[:class:`GuildSticker`]
+
.. function:: on_guild_available(guild)
on_guild_unavailable(guild)
@@ -2205,6 +2220,63 @@ of :class:`enum.Enum`.
.. versionadded:: 2.0
+ .. attribute:: sticker_create
+
+ A sticker was created.
+
+ When this is the action, the type of :attr:`~AuditLogEntry.target` is
+ the :class:`GuildSticker` or :class:`Object` with the ID of the sticker
+ which was updated.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.emoji`
+ - :attr:`~AuditLogDiff.type`
+ - :attr:`~AuditLogDiff.format_type`
+ - :attr:`~AuditLogDiff.description`
+ - :attr:`~AuditLogDiff.available`
+
+ .. versionadded:: 2.0
+
+ .. attribute:: sticker_update
+
+ A sticker was updated.
+
+ When this is the action, the type of :attr:`~AuditLogEntry.target` is
+ the :class:`GuildSticker` or :class:`Object` with the ID of the sticker
+ which was updated.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.emoji`
+ - :attr:`~AuditLogDiff.type`
+ - :attr:`~AuditLogDiff.format_type`
+ - :attr:`~AuditLogDiff.description`
+ - :attr:`~AuditLogDiff.available`
+
+ .. versionadded:: 2.0
+
+ .. attribute:: sticker_delete
+
+ A sticker was deleted.
+
+ When this is the action, the type of :attr:`~AuditLogEntry.target` is
+ the :class:`GuildSticker` or :class:`Object` with the ID of the sticker
+ which was updated.
+
+ Possible attributes for :class:`AuditLogDiff`:
+
+ - :attr:`~AuditLogDiff.name`
+ - :attr:`~AuditLogDiff.emoji`
+ - :attr:`~AuditLogDiff.type`
+ - :attr:`~AuditLogDiff.format_type`
+ - :attr:`~AuditLogDiff.description`
+ - :attr:`~AuditLogDiff.available`
+
+ .. versionadded:: 2.0
+
.. attribute:: thread_create
A thread was created.
@@ -2356,6 +2428,20 @@ of :class:`enum.Enum`.
.. class:: StickerType
+ Represents the type of sticker.
+
+ .. versionadded:: 2.0
+
+ .. attribute:: standard
+
+ Represents a standard sticker that all Nitro users can use.
+
+ .. attribute:: guild
+
+ Represents a custom sticker created in a guild.
+
+.. class:: StickerFormatType
+
Represents the type of sticker images.
.. versionadded:: 1.6
@@ -2825,15 +2911,9 @@ AuditLogDiff
.. attribute:: type
- The type of channel or channel permission overwrite.
+ The type of channel or sticker.
- If the type is an :class:`int`, then it is a type of channel which can be either
- ``0`` to indicate a text channel or ``1`` to indicate a voice channel.
-
- If the type is a :class:`str`, then it is a type of permission overwrite which
- can be either ``'role'`` or ``'member'``.
-
- :type: Union[:class:`int`, :class:`str`]
+ :type: Union[:class:`ChannelType`, :class:`StickerType`]
.. attribute:: topic
@@ -3040,6 +3120,38 @@ AuditLogDiff
:type: :class:`VideoQualityMode`
+ .. attribute:: format_type
+
+ The format type of a sticker being changed.
+
+ See also :attr:`GuildSticker.format_type`
+
+ :type: :class:`StickerFormatType`
+
+ .. attribute:: emoji
+
+ The name of the emoji that represents a sticker being changed.
+
+ See also :attr:`GuildSticker.emoji`
+
+ :type: :class:`str`
+
+ .. attribute:: description
+
+ The description of a sticker being changed.
+
+ See also :attr:`GuildSticker.description`
+
+ :type: :class:`str`
+
+ .. attribute:: available
+
+ The availability of a sticker being changed.
+
+ See also :attr:`GuildSticker.available`
+
+ :type: :class:`bool`
+
.. attribute:: archived
The thread is now archived.
@@ -3620,6 +3732,22 @@ Widget
.. autoclass:: Widget()
:members:
+StickerPack
+~~~~~~~~~~~~~
+
+.. attributetable:: StickerPack
+
+.. autoclass:: StickerPack()
+ :members:
+
+StickerItem
+~~~~~~~~~~~~~
+
+.. attributetable:: StickerItem
+
+.. autoclass:: StickerItem()
+ :members:
+
Sticker
~~~~~~~~~~~~~~~
@@ -3628,6 +3756,22 @@ Sticker
.. autoclass:: Sticker()
:members:
+StandardSticker
+~~~~~~~~~~~~~~~~
+
+.. attributetable:: StandardSticker
+
+.. autoclass:: StandardSticker()
+ :members:
+
+GuildSticker
+~~~~~~~~~~~~~
+
+.. attributetable:: GuildSticker
+
+.. autoclass:: GuildSticker()
+ :members:
+
RawMessageDeleteEvent
~~~~~~~~~~~~~~~~~~~~~~~