diff --git a/discord/abc.py b/discord/abc.py index a2f4a847c..2ed7b5136 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, @@ -1304,6 +1309,10 @@ class Messageable: embeds: List[:class:`~discord.Embed`] 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 eafff466d..8aa439144 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 2c55370e4..1870620f0 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 298dff3ad..9c5c7dbff 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 @@ -277,6 +278,14 @@ class Client: """List[:class:`.Emoji`]: The emojis that the connected client has.""" 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 d2e682f3e..80af39ece 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 373a89576..1a3ca792a 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 c65296db7..c61ed7f05 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') @@ -698,6 +709,15 @@ class Guild(Hashable): more_emoji = 200 if 'MORE_EMOJI' in self.features else 50 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.""" @@ -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 fd0f1e783..7c18b7016 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 82f812db9..270ac9e9e 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 ab7ecbfe2..04c475fa9 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 fd0587bf8..ec8ce94ce 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 235b66f2a..b93f5baf3 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'' + + 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'' + + 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'' 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'' + + 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'' + + @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 e784e7df4..563819fd3 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 2dbdf9839..5448a56a4 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 000000000..111505934 --- /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 2f4d911bf..1c50190b3 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 ~~~~~~~~~~~~~~~~~~~~~~~