diff --git a/discord/appinfo.py b/discord/appinfo.py index 6f1754ffa..db60502f2 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -49,8 +49,6 @@ class AppInfo: .. versionadded:: 1.3 - icon: Optional[:class:`str`] - The icon hash, if it exists. description: Optional[:class:`str`] The application description. bot_public: :class:`bool` @@ -88,12 +86,6 @@ class AppInfo: If this application is a game sold on Discord, this field will be the URL slug that links to the store page - .. versionadded:: 1.3 - - cover_image: Optional[:class:`str`] - If this application is a game sold on Discord, - this field will be the hash of the image on store embeds - .. versionadded:: 1.3 """ @@ -106,14 +98,14 @@ class AppInfo: 'bot_public', 'bot_require_code_grant', 'owner', - 'icon', + '_icon', 'summary', 'verify_key', 'team', 'guild_id', 'primary_sku_id', 'slug', - 'cover_image', + '_cover_image', ) def __init__(self, state, data): @@ -122,7 +114,7 @@ class AppInfo: self.id = int(data['id']) self.name = data['name'] self.description = data['description'] - self.icon = data['icon'] + self._icon = data['icon'] self.rpc_origins = data['rpc_origins'] self.bot_public = data['bot_public'] self.bot_require_code_grant = data['bot_require_code_grant'] @@ -138,7 +130,7 @@ class AppInfo: self.primary_sku_id = utils._get_as_snowflake(data, 'primary_sku_id') self.slug = data.get('slug') - self.cover_image = data.get('cover_image') + self._cover_image = data.get('cover_image') def __repr__(self): return ( @@ -148,81 +140,21 @@ class AppInfo: ) @property - def icon_url(self): - """:class:`.Asset`: Retrieves the application's icon asset. - - This is equivalent to calling :meth:`icon_url_as` with - the default parameters ('webp' format and a size of 1024). - - .. versionadded:: 1.3 - """ - return self.icon_url_as() - - def icon_url_as(self, *, format='webp', size=1024): - """Returns an :class:`Asset` for the icon the application has. - - The format must be one of 'webp', 'jpeg', 'jpg' or 'png'. - The size must be a power of 2 between 16 and 4096. - - .. versionadded:: 1.6 - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the icon to. Defaults to 'webp'. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_icon(self._state, self, 'app', format=format, size=size) + def icon(self): + """Optional[:class:`.Asset`]: Retrieves the application's icon asset, if any.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path='app') @property - def cover_image_url(self): - """:class:`.Asset`: Retrieves the cover image on a store embed. - - This is equivalent to calling :meth:`cover_image_url_as` with - the default parameters ('webp' format and a size of 1024). - - .. versionadded:: 1.3 - """ - return self.cover_image_url_as() - - def cover_image_url_as(self, *, format='webp', size=1024): - """Returns an :class:`Asset` for the image on store embeds - if this application is a game sold on Discord. - - The format must be one of 'webp', 'jpeg', 'jpg' or 'png'. - The size must be a power of 2 between 16 and 4096. - - .. versionadded:: 1.6 - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the image to. Defaults to 'webp'. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. + def cover_image(self): + """Optional[:class:`.Asset`]: Retrieves the cover image on a store embed, if any. - Returns - -------- - :class:`Asset` - The resulting CDN asset. + This is only available if the application is a game sold on Discord. """ - return Asset._from_cover_image(self._state, self, format=format, size=size) + if self._cover_image is None: + return None + return Asset._from_cover_image(self._state, self.id, self._cover_image) @property def guild(self): diff --git a/discord/asset.py b/discord/asset.py index 13f9336f1..bf7cd6ca1 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -22,22 +22,28 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + import io -from typing import Literal, TYPE_CHECKING +import os +from typing import BinaryIO, Literal, TYPE_CHECKING, Tuple, Union from .errors import DiscordException from .errors import InvalidArgument from . import utils +import yarl + __all__ = ( 'Asset', ) if TYPE_CHECKING: ValidStaticFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png'] - ValidAvatarFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif'] + ValidAssetFormatTypes = Literal['webp', 'jpeg', 'jpg', 'png', 'gif'] VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) -VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"} +VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"} + class Asset: """Represents a CDN asset on Discord. @@ -52,10 +58,6 @@ class Asset: Returns the length of the CDN asset's URL. - .. describe:: bool(x) - - Checks if the Asset has a URL. - .. describe:: x == y Checks if the asset is equal to another asset. @@ -68,96 +70,88 @@ class Asset: Returns the hash of the asset. """ - __slots__ = ('_state', '_url') + + __slots__: Tuple[str, ...] = ( + '_state', + '_url', + '_animated', + '_key', + ) BASE = 'https://cdn.discordapp.com' - def __init__(self, state, url=None): + def __init__(self, state, *, url: str, key: str, animated: bool = False): self._state = state self._url = url + self._animated = animated + self._key = key @classmethod - def _from_avatar(cls, state, user, *, format=None, static_format='webp', size=1024): - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") - if format is not None and format not in VALID_AVATAR_FORMATS: - raise InvalidArgument(f"format must be None or one of {VALID_AVATAR_FORMATS}") - if format == "gif" and not user.is_avatar_animated(): - raise InvalidArgument("non animated avatars do not support gif format") - if static_format not in VALID_STATIC_FORMATS: - raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}") - - if user.avatar is None: - return user.default_avatar_url - - if format is None: - format = 'gif' if user.is_avatar_animated() else static_format - - return cls(state, f'/avatars/{user.id}/{user.avatar}.{format}?size={size}') + def _from_default_avatar(cls, state, index: int) -> Asset: + return cls( + state, + url=f'{cls.BASE}/embed/avatars/{index}.png', + key=str(index), + animated=False, + ) @classmethod - def _from_icon(cls, state, object, path, *, format='webp', size=1024): - if object.icon is None: - return cls(state) - - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") - if format not in VALID_STATIC_FORMATS: - raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}") - - url = f'/{path}-icons/{object.id}/{object.icon}.{format}?size={size}' - return cls(state, url) + def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: + animated = avatar.startswith('a_') + format = 'gif' if animated else 'png' + return cls( + state, + url=f'{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024', + key=avatar, + animated=animated, + ) @classmethod - def _from_cover_image(cls, state, obj, *, format='webp', size=1024): - if obj.cover_image is None: - return cls(state) - - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") - if format not in VALID_STATIC_FORMATS: - raise InvalidArgument(f"format must be None or one of {VALID_STATIC_FORMATS}") - - url = f'/app-assets/{obj.id}/store/{obj.cover_image}.{format}?size={size}' - return cls(state, url) + def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: + return cls( + state, + url=f'{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024', + key=icon_hash, + animated=False, + ) @classmethod - def _from_guild_image(cls, state, id, hash, key, *, format='webp', size=1024): - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") - if format not in VALID_STATIC_FORMATS: - raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}") - - if hash is None: - return cls(state) - - return cls(state, f'/{key}/{id}/{hash}.{format}?size={size}') + def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset: + return cls( + state, + url=f'{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024', + key=cover_image_hash, + animated=False, + ) @classmethod - def _from_guild_icon(cls, state, guild, *, format=None, static_format='webp', size=1024): - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") - if format is not None and format not in VALID_AVATAR_FORMATS: - raise InvalidArgument(f"format must be one of {VALID_AVATAR_FORMATS}") - if format == "gif" and not guild.is_icon_animated(): - raise InvalidArgument("non animated guild icons do not support gif format") - if static_format not in VALID_STATIC_FORMATS: - raise InvalidArgument(f"static_format must be one of {VALID_STATIC_FORMATS}") - - if guild.icon is None: - return cls(state) - - if format is None: - format = 'gif' if guild.is_icon_animated() else static_format - - return cls(state, f'/icons/{guild.id}/{guild.icon}.{format}?size={size}') + def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset: + return cls( + state, + url=f'{cls.BASE}/{path}/{guild_id}/{image}.png?size=1024', + key=image, + animated=False, + ) @classmethod - def _from_sticker_url(cls, state, sticker, *, size=1024): - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 4096") + def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: + animated = icon_hash.startswith('a_') + format = 'gif' if animated else 'png' + return cls( + state, + url=f'{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024', + key=icon_hash, + animated=animated, + ) - return cls(state, f'/stickers/{sticker.id}/{sticker.image}.png?size={size}') + @classmethod + def _from_sticker(cls, state, sticker_id: int, sticker_hash: str) -> Asset: + return cls( + state, + url=f'{cls.BASE}/stickers/{sticker_id}/{sticker_hash}.png?size=1024', + key=sticker_hash, + animated=False, + ) @classmethod def _from_emoji(cls, state, emoji, *, format=None, static_format='png'): @@ -172,46 +166,184 @@ class Asset: return cls(state, f'/emojis/{emoji.id}.{format}') - def __str__(self): - return self.BASE + self._url if self._url is not None else '' - - def __len__(self): - if self._url: - return len(self.BASE + self._url) - return 0 + def __str__(self) -> str: + return self._url - def __bool__(self): - return self._url is not None + def __len__(self) -> int: + return len(self._url) def __repr__(self): - return f'' + shorten = self._url.replace(self.BASE, '') + return f'' def __eq__(self, other): return isinstance(other, Asset) and self._url == other._url - def __ne__(self, other): - return not self.__eq__(other) - def __hash__(self): return hash(self._url) - async def read(self): - """|coro| + @property + def url(self) -> str: + """:class:`str`: Returns the underlying URL of the asset.""" + return self._url - Retrieves the content of this asset as a :class:`bytes` object. + @property + def key(self) -> str: + """:class:`str`: Returns the identifying key of the asset.""" + return self._key - .. warning:: + def is_animated(self) -> bool: + """:class:`bool`: Returns whether the asset is animated.""" + return self._animated - :class:`PartialEmoji` won't have a connection state if user created, - and a URL won't be present if a custom image isn't associated with - the asset, e.g. a guild with no custom icon. + def replace( + self, + size: int = ..., + format: ValidAssetFormatTypes = ..., + static_format: ValidStaticFormatTypes = ..., + ) -> Asset: + """Returns a new asset with the passed components replaced. + + Parameters + ----------- + size: :class:`int` + The new size of the asset. + format: :class:`str` + The new format to change it to. Must be either + 'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated. + static_format: :class:`str` + The new format to change it to if the asset isn't animated. + Must be either 'webp', 'jpeg', 'jpg', or 'png'. + + Raises + ------- + InvalidArgument + An invalid size or format was passed. + + Returns + -------- + :class:`Asset` + The newly updated asset. + """ + url = yarl.URL(self._url) + path, _ = os.path.splitext(url.path) + + if format is not ...: + if self._animated: + if format not in VALID_ASSET_FORMATS: + raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}') + else: + if format not in VALID_STATIC_FORMATS: + raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}') + url = url.with_path(f'{path}.{format}') + + if static_format is not ... and not self._animated: + if static_format not in VALID_STATIC_FORMATS: + raise InvalidArgument(f'static_format must be one of {VALID_STATIC_FORMATS}') + url = url.with_path(f'{path}.{static_format}') + + if size is not ...: + if not utils.valid_icon_size(size): + raise InvalidArgument('size must be a power of 2 between 16 and 4096') + url = url.with_query(size=size) + else: + url = url.with_query(url.raw_query_string) + + url = str(url) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_size(self, size: int) -> Asset: + """Returns a new asset with the specified size. + + Parameters + ------------ + size: :class:`int` + The new size of the asset. + + Raises + ------- + InvalidArgument + The asset had an invalid size. + + Returns + -------- + :class:`Asset` + The new updated asset. + """ + if not utils.valid_icon_size(size): + raise InvalidArgument('size must be a power of 2 between 16 and 4096') + + url = str(yarl.URL(self._url).with_query(size=size)) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_format(self, format: ValidAssetFormatTypes) -> Asset: + """Returns a new asset with the specified format. + + Parameters + ------------ + format: :class:`str` + The new format of the asset. + + Raises + ------- + InvalidArgument + The asset had an invalid format. + + Returns + -------- + :class:`Asset` + The new updated asset. + """ + + if self._animated: + if format not in VALID_ASSET_FORMATS: + raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}') + else: + if format not in VALID_STATIC_FORMATS: + raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}') + + url = yarl.URL(self._url) + path, _ = os.path.splitext(url.path) + url = str(url.with_path(f'{path}.{format}').with_query(url.raw_query_string)) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_static_format(self, format: ValidStaticFormatTypes) -> Asset: + """Returns a new asset with the specified static format. + + This only changes the format if the underlying asset is + not animated. Otherwise, the asset is not changed. + + Parameters + ------------ + format: :class:`str` + The new static format of the asset. + + Raises + ------- + InvalidArgument + The asset had an invalid format. + + Returns + -------- + :class:`Asset` + The new updated asset. + """ + + if self._animated: + return self + return self.with_format(format) + + async def read(self) -> bytes: + """|coro| + + Retrieves the content of this asset as a :class:`bytes` object. .. versionadded:: 1.1 Raises ------ DiscordException - There was no valid URL or internal connection state. + There was no internal connection state. HTTPException Downloading the asset failed. NotFound @@ -222,15 +354,12 @@ class Asset: :class:`bytes` The content of the asset. """ - if not self._url: - raise DiscordException('Invalid asset (no URL provided)') - if self._state is None: raise DiscordException('Invalid state (no ConnectionState provided)') return await self._state.http.get_from_cdn(self.BASE + self._url) - async def save(self, fp, *, seek_begin=True): + async def save(self, fp: Union[str, bytes, os.PathLike, BinaryIO], *, seek_begin: bool = True) -> int: """|coro| Saves this asset into a file-like object. @@ -245,7 +374,7 @@ class Asset: Raises ------ DiscordException - There was no valid URL or internal connection state. + There was no internal connection state. HTTPException Downloading the asset failed. NotFound diff --git a/discord/channel.py b/discord/channel.py index ee6869c0b..57ec56ca1 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -94,9 +94,9 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): :attr:`~Permissions.manage_messages` bypass slowmode. nsfw: :class:`bool` If the channel is marked as "not safe for work". - + .. note:: - + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. """ @@ -894,9 +894,9 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): top category is position 0. nsfw: :class:`bool` If the channel is marked as "not safe for work". - + .. note:: - + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. """ @@ -1096,9 +1096,9 @@ class StoreChannel(discord.abc.GuildChannel, Hashable): top channel is position 0. nsfw: :class:`bool` If the channel is marked as "not safe for work". - + .. note:: - + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. """ __slots__ = ('name', 'id', 'guild', '_state', 'nsfw', @@ -1343,13 +1343,11 @@ class GroupChannel(discord.abc.Messageable, Hashable): The group channel ID. owner: :class:`User` The user that owns the group channel. - icon: Optional[:class:`str`] - The group channel's icon hash if provided. name: Optional[:class:`str`] The group channel's name if provided. """ - __slots__ = ('id', 'recipients', 'owner', 'icon', 'name', 'me', '_state') + __slots__ = ('id', 'recipients', 'owner', '_icon', 'name', 'me', '_state') def __init__(self, *, me, state, data): self._state = state @@ -1359,7 +1357,7 @@ class GroupChannel(discord.abc.Messageable, Hashable): def _update_group(self, data): owner_id = utils._get_as_snowflake(data, 'owner_id') - self.icon = data.get('icon') + self._icon = data.get('icon') self.name = data.get('name') try: @@ -1393,40 +1391,11 @@ class GroupChannel(discord.abc.Messageable, Hashable): return ChannelType.group @property - def icon_url(self): - """:class:`Asset`: Returns the channel's icon asset if available. - - This is equivalent to calling :meth:`icon_url_as` with - the default parameters ('webp' format and a size of 1024). - """ - return self.icon_url_as() - - def icon_url_as(self, *, format='webp', size=1024): - """Returns an :class:`Asset` for the icon the channel has. - - The format must be one of 'webp', 'jpeg', 'jpg' or 'png'. - The size must be a power of 2 between 16 and 4096. - - .. versionadded:: 2.0 - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the icon to. Defaults to 'webp'. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_icon(self._state, self, 'channel', format=format, size=size) + def icon(self): + """Optional[:class:`Asset`]: Returns the channel's icon asset if available.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path='channel') @property def created_at(self): diff --git a/discord/emoji.py b/discord/emoji.py index bf7982ab7..aba6d765e 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -22,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io + from .asset import Asset from . import utils from .partial_emoji import _EmojiTag @@ -132,13 +134,10 @@ class Emoji(_EmojiTag): return utils.snowflake_time(self.id) @property - def url(self): - """:class:`Asset`: Returns the asset of the emoji. - - This is equivalent to calling :meth:`url_as` with - the default parameters (i.e. png/gif detection). - """ - return self.url_as(format=None) + def url(self) -> str: + """:class:`str`: Returns the URL of the emoji.""" + fmt = 'gif' if self.animated else 'png' + return f'{Asset.BASE}/emojis/{self.id}.{fmt}' @property def roles(self): @@ -157,39 +156,6 @@ class Emoji(_EmojiTag): """:class:`Guild`: The guild this emoji belongs to.""" return self._state._get_guild(self.guild_id) - - def url_as(self, *, format=None, static_format="png"): - """Returns an :class:`Asset` for the emoji's url. - - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'. - 'gif' is only valid for animated emojis. - - .. versionadded:: 1.6 - - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the emojis to. - If the format is ``None``, then it is automatically - detected as either 'gif' or static_format, depending on whether the - emoji is animated or not. - static_format: Optional[:class:`str`] - Format to attempt to convert only non-animated emoji's to. - Defaults to 'png' - - Raises - ------- - InvalidArgument - Bad image format passed to ``format`` or ``static_format``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_emoji(self._state, self, format=format, static_format=static_format) - - def is_usable(self): """:class:`bool`: Whether the bot can use this emoji. @@ -254,3 +220,61 @@ class Emoji(_EmojiTag): if roles: roles = [role.id for role in roles] await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason) + + async def read(self): + """|coro| + + Retrieves the content of this emoji as a :class:`bytes` object. + + .. versionadded:: 2.0 + + Raises + ------ + HTTPException + Downloading the emoji failed. + NotFound + The emoji was deleted. + + Returns + ------- + :class:`bytes` + The content of the emoji. + """ + return await self._state.http.get_from_cdn(self.url) + + async def save(self, fp, *, seek_begin=True): + """|coro| + + Saves this emoji into a file-like object. + + .. versionadded:: 2.0 + + Parameters + ---------- + fp: Union[BinaryIO, :class:`os.PathLike`] + Same as in :meth:`Attachment.save`. + seek_begin: :class:`bool` + Same as in :meth:`Attachment.save`. + + Raises + ------ + HTTPException + Downloading the emoji failed. + NotFound + The emoji was deleted. + + Returns + -------- + :class:`int` + The number of bytes written. + """ + + data = await self.read() + if isinstance(fp, io.IOBase) and fp.writable(): + written = fp.write(data) + if seek_begin: + fp.seek(0) + return written + else: + with open(fp, 'wb') as f: + return f.write(data) diff --git a/discord/flags.py b/discord/flags.py index cb268d09b..5e182f8ff 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -504,7 +504,7 @@ class Intents(BaseFlags): - :attr:`Member.nick` - :attr:`Member.premium_since` - :attr:`User.name` - - :attr:`User.avatar` (:attr:`User.avatar_url` and :meth:`User.avatar_url_as`) + - :attr:`User.avatar` - :attr:`User.discriminator` For more information go to the :ref:`member intent documentation `. diff --git a/discord/guild.py b/discord/guild.py index 3f63d8739..20eff90ba 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -94,8 +94,6 @@ class Guild(Hashable): The timeout to get sent to the AFK channel. afk_channel: Optional[:class:`VoiceChannel`] The channel that denotes the AFK channel. ``None`` if it doesn't exist. - icon: Optional[:class:`str`] - The guild's icon. id: :class:`int` The guild's ID. owner_id: :class:`int` @@ -118,8 +116,6 @@ class Guild(Hashable): The maximum amount of users in a video channel. .. versionadded:: 1.4 - banner: Optional[:class:`str`] - The guild's banner. description: Optional[:class:`str`] The guild's description. mfa_level: :class:`int` @@ -154,8 +150,6 @@ class Guild(Hashable): - ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled. - ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening. - splash: Optional[:class:`str`] - The guild's invite splash. premium_tier: :class:`int` The premium tier for this guild. Corresponds to "Nitro Server" in the official UI. The number goes from 0 to 3 inclusive. @@ -164,10 +158,6 @@ class Guild(Hashable): preferred_locale: Optional[:class:`str`] The preferred locale for the guild. Used when filtering Server Discovery results to a specific language. - discovery_splash: :class:`str` - The guild's discovery splash. - - .. versionadded:: 1.3 nsfw: :class:`bool` If the guild is marked as "not safe for work". @@ -175,15 +165,15 @@ class Guild(Hashable): .. versionadded:: 2.0 """ - __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', 'icon', - 'name', 'id', 'unavailable', 'banner', 'region', '_state', + __slots__ = ('afk_timeout', 'afk_channel', '_members', '_channels', '_icon', + 'name', 'id', 'unavailable', '_banner', 'region', '_state', '_roles', '_member_count', '_large', 'owner_id', 'mfa_level', 'emojis', 'features', - 'verification_level', 'explicit_content_filter', 'splash', + 'verification_level', 'explicit_content_filter', '_splash', '_voice_states', '_system_channel_id', 'default_notifications', 'description', 'max_presences', 'max_members', 'max_video_channel_users', 'premium_tier', 'premium_subscription_count', '_system_channel_flags', - 'preferred_locale', 'discovery_splash', '_rules_channel_id', + 'preferred_locale', '_discovery_splash', '_rules_channel_id', '_public_updates_channel_id', 'nsfw') _PREMIUM_GUILD_LIMITS = { @@ -293,8 +283,8 @@ class Guild(Hashable): self.default_notifications = try_enum(NotificationLevel, guild.get('default_message_notifications')) self.explicit_content_filter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0)) self.afk_timeout = guild.get('afk_timeout') - self.icon = guild.get('icon') - self.banner = guild.get('banner') + self._icon = guild.get('icon') + self._banner = guild.get('banner') self.unavailable = guild.get('unavailable', False) self.id = int(guild['id']) self._roles = {} @@ -306,7 +296,7 @@ class Guild(Hashable): self.mfa_level = guild.get('mfa_level') self.emojis = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', []))) self.features = guild.get('features', []) - self.splash = guild.get('splash') + self._splash = guild.get('splash') self._system_channel_id = utils._get_as_snowflake(guild, 'system_channel_id') self.description = guild.get('description') self.max_presences = guild.get('max_presences') @@ -316,7 +306,7 @@ class Guild(Hashable): self.premium_subscription_count = guild.get('premium_subscription_count') or 0 self._system_channel_flags = guild.get('system_channel_flags', 0) self.preferred_locale = guild.get('preferred_locale') - self.discovery_splash = guild.get('discovery_splash') + self._discovery_splash = guild.get('discovery_splash') self._rules_channel_id = utils._get_as_snowflake(guild, 'rules_channel_id') self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id') self.nsfw = guild.get('nsfw', False) @@ -621,139 +611,32 @@ class Guild(Hashable): return self.get_member(self.owner_id) @property - def icon_url(self): - """:class:`Asset`: Returns the guild's icon asset.""" - return self.icon_url_as() - - def is_icon_animated(self): - """:class:`bool`: Returns True if the guild has an animated icon.""" - return bool(self.icon and self.icon.startswith('a_')) - - def icon_url_as(self, *, format=None, static_format='webp', size=1024): - """Returns an :class:`Asset` for the guild's icon. - - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and - 'gif' is only valid for animated avatars. The size must be a power of 2 - between 16 and 4096. - - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the icon to. - If the format is ``None``, then it is automatically - detected into either 'gif' or static_format depending on the - icon being animated or not. - static_format: Optional[:class:`str`] - Format to attempt to convert only non-animated icons to. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size) + def icon(self): + """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + if self._icon is None: + return None + return Asset._from_guild_icon(self._state, self.id, self._icon) @property - def banner_url(self): - """:class:`Asset`: Returns the guild's banner asset.""" - return self.banner_url_as() - - def banner_url_as(self, *, format='webp', size=2048): - """Returns an :class:`Asset` for the guild's banner. - - The format must be one of 'webp', 'jpeg', or 'png'. The - size must be a power of 2 between 16 and 4096. - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the banner to. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size) + def banner(self): + """Optional[:class:`Asset`]: Returns the guild's banner asset, if available.""" + if self._banner is None: + return None + return Asset._from_guild_image(self._state, self.id, self._banner, path='banners') @property - def splash_url(self): - """:class:`Asset`: Returns the guild's invite splash asset.""" - return self.splash_url_as() - - def splash_url_as(self, *, format='webp', size=2048): - """Returns an :class:`Asset` for the guild's invite splash. - - The format must be one of 'webp', 'jpeg', 'jpg', or 'png'. The - size must be a power of 2 between 16 and 4096. - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the splash to. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size) + def splash(self): + """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + if self._splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes') @property - def discovery_splash_url(self): - """:class:`Asset`: Returns the guild's discovery splash asset. - - .. versionadded:: 1.3 - """ - return self.discovery_splash_url_as() - - def discovery_splash_url_as(self, *, format='webp', size=2048): - """Returns an :class:`Asset` for the guild's discovery splash. - - The format must be one of 'webp', 'jpeg', 'jpg', or 'png'. The - size must be a power of 2 between 16 and 4096. - - .. versionadded:: 1.3 - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the splash to. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_image(self._state, self.id, self.discovery_splash, 'discovery-splashes', format=format, size=size) + def discovery_splash(self): + """Optional[:class:`Asset`]: Returns the guild's discovery splash asset, if available.""" + if self._discovery_splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._discovery_splash, path='discovery-splashes') @property def member_count(self): @@ -1185,7 +1068,7 @@ class Guild(Hashable): try: icon_bytes = fields['icon'] except KeyError: - icon = self.icon + icon = self._icon else: if icon_bytes is not None: icon = utils._bytes_to_base64_data(icon_bytes) @@ -1195,7 +1078,7 @@ class Guild(Hashable): try: banner_bytes = fields['banner'] except KeyError: - banner = self.banner + banner = self._banner else: if banner_bytes is not None: banner = utils._bytes_to_base64_data(banner_bytes) @@ -1212,7 +1095,7 @@ class Guild(Hashable): try: splash_bytes = fields['splash'] except KeyError: - splash = self.splash + splash = self._splash else: if splash_bytes is not None: splash = utils._bytes_to_base64_data(splash_bytes) diff --git a/discord/invite.py b/discord/invite.py index d8bd554fe..ce601299d 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -140,26 +140,20 @@ class PartialInviteGuild: The partial guild's verification level. features: List[:class:`str`] A list of features the guild has. See :attr:`Guild.features` for more information. - icon: Optional[:class:`str`] - The partial guild's icon. - banner: Optional[:class:`str`] - The partial guild's banner. - splash: Optional[:class:`str`] - The partial guild's invite splash. description: Optional[:class:`str`] The partial guild's description. """ - __slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash', 'verification_level', 'description') + __slots__ = ('_state', 'features', '_icon', '_banner', 'id', 'name', '_splash', 'verification_level', 'description') def __init__(self, state, data: InviteGuildPayload, id: int): self._state = state self.id = id self.name = data['name'] self.features = data.get('features', []) - self.icon = data.get('icon') - self.banner = data.get('banner') - self.splash = data.get('splash') + self._icon = data.get('icon') + self._banner = data.get('banner') + self._splash = data.get('splash') self.verification_level = try_enum(VerificationLevel, data.get('verification_level')) self.description = data.get('description') @@ -178,56 +172,25 @@ class PartialInviteGuild: return snowflake_time(self.id) @property - def icon_url(self) -> Asset: - """:class:`Asset`: Returns the guild's icon asset.""" - return self.icon_url_as() - - def is_icon_animated(self) -> bool: - """:class:`bool`: Returns ``True`` if the guild has an animated icon. - - .. versionadded:: 1.4 - """ - return bool(self.icon and self.icon.startswith('a_')) - - def icon_url_as(self, *, format=None, static_format='webp', size=1024) -> Asset: - """The same operation as :meth:`Guild.icon_url_as`. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size) + def icon_url(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + if self._icon is None: + return None + return Asset._from_guild_icon(self._state, self.id, self._icon) @property - def banner_url(self) -> Asset: - """:class:`Asset`: Returns the guild's banner asset.""" - return self.banner_url_as() - - def banner_url_as(self, *, format='webp', size=2048) -> Asset: - """The same operation as :meth:`Guild.banner_url_as`. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size) + def banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's banner asset, if available.""" + if self._banner is None: + return None + return Asset._from_guild_image(self._state, self.id, self._banner, path='banners') @property - def splash_url(self) -> Asset: - """:class:`Asset`: Returns the guild's invite splash asset.""" - return self.splash_url_as() - - def splash_url_as(self, *, format='webp', size=2048) -> Asset: - """The same operation as :meth:`Guild.splash_url_as`. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size) + def splash(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's invite splash asset, if available.""" + if self._splash is None: + return None + return Asset._from_guild_image(self._state, self.id, self._splash, path='splashes') class Invite(Hashable): diff --git a/discord/member.py b/discord/member.py index 01cb299c8..b6f98282f 100644 --- a/discord/member.py +++ b/discord/member.py @@ -325,12 +325,12 @@ class Member(discord.abc.Messageable, _BaseUser): def _update_inner_user(self, user): u = self._user - original = (u.name, u.avatar, u.discriminator, u._public_flags) + original = (u.name, u._avatar, u.discriminator, u._public_flags) # These keys seem to always be available modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0)) if original != modified: to_return = User._copy(self._user) - u.name, u.avatar, u.discriminator, u._public_flags = modified + u.name, u._avatar, u.discriminator, u._public_flags = modified # Signal to dispatch on_user_update return to_return, u @@ -442,7 +442,7 @@ class Member(discord.abc.Messageable, _BaseUser): .. note:: - Due to a Discord API limitation, this may be ``None`` if + Due to a Discord API limitation, this may be ``None`` if the user is listening to a song on Spotify with a title longer than 128 characters. See :issue:`1738` for more information. diff --git a/discord/message.py b/discord/message.py index eadd03e52..97d42704a 100644 --- a/discord/message.py +++ b/discord/message.py @@ -172,11 +172,11 @@ class Attachment(Hashable): The number of bytes written. """ data = await self.read(use_cached=use_cached) - if isinstance(fp, io.IOBase) and fp.writable(): + if isinstance(fp, io.RawIOBase): written = fp.write(data) if seek_begin: fp.seek(0) - return written + return written or 0 else: with open(fp, 'wb') as f: return f.write(data) diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index c84da4bf1..0c65dc341 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io + from .asset import Asset +from .errors import DiscordException, InvalidArgument from . import utils __all__ = ( @@ -149,44 +152,83 @@ class PartialEmoji(_EmojiTag): return utils.snowflake_time(self.id) @property - def url(self): - """:class:`Asset`: Returns the asset of the emoji, if it is custom. + def url(self) -> str: + """:class:`str`: Returns the URL of the emoji, if it is custom. - This is equivalent to calling :meth:`url_as` with - the default parameters (i.e. png/gif detection). + If this isn't a custom emoji then an empty string is returned """ - return self.url_as(format=None) + if self.is_unicode_emoji(): + return '' - def url_as(self, *, format=None, static_format="png"): - """Returns an :class:`Asset` for the emoji's url, if it is custom. + fmt = 'gif' if self.animated else 'png' + return f'{Asset.BASE}/emojis/{self.id}.{fmt}' - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'. - 'gif' is only valid for animated emojis. + async def read(self): + """|coro| - .. versionadded:: 1.7 + Retrieves the content of this emoji as a :class:`bytes` object. - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the emojis to. - If the format is ``None``, then it is automatically - detected as either 'gif' or static_format, depending on whether the - emoji is animated or not. - static_format: Optional[:class:`str`] - Format to attempt to convert only non-animated emoji's to. - Defaults to 'png' + .. versionadded:: 2.0 Raises - ------- + ------ + DiscordException + There was no internal connection state. InvalidArgument - Bad image format passed to ``format`` or ``static_format``. + The emoji isn't custom. + HTTPException + Downloading the emoji failed. + NotFound + The emoji was deleted. Returns - -------- - :class:`Asset` - The resulting CDN asset. + ------- + :class:`bytes` + The content of the emoji. """ + if self._state is None: + raise DiscordException('Invalid state (no ConnectionState provided)') + if self.is_unicode_emoji(): - return Asset(self._state) + raise InvalidArgument('PartialEmoji is not a custom emoji') + + return await self._state.http.get_from_cdn(self.url) + + async def save(self, fp, *, seek_begin=True): + """|coro| + + Saves this emoji into a file-like object. + + .. versionadded:: 2.0 + + Parameters + ---------- + fp: Union[BinaryIO, :class:`os.PathLike`] + Same as in :meth:`Attachment.save`. + seek_begin: :class:`bool` + Same as in :meth:`Attachment.save`. + + Raises + ------ + DiscordException + There was no internal connection state. + HTTPException + Downloading the emoji failed. + NotFound + The emoji was deleted. + + Returns + -------- + :class:`int` + The number of bytes written. + """ - return Asset._from_emoji(self._state, self, format=format, static_format=static_format) + data = await self.read() + if isinstance(fp, io.IOBase) and fp.writable(): + written = fp.write(data) + if seek_begin: + fp.seek(0) + return written + else: + with open(fp, 'wb') as f: + return f.write(data) diff --git a/discord/sticker.py b/discord/sticker.py index 0b3c66d97..8a1e4518d 100644 --- a/discord/sticker.py +++ b/discord/sticker.py @@ -62,12 +62,10 @@ class Sticker(Hashable): The id of the sticker's pack. format: :class:`StickerType` The format for the sticker's image. - image: :class:`str` - The sticker's image. tags: List[:class:`str`] A list of tags for the sticker. """ - __slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', 'image', 'tags') + __slots__ = ('_state', 'id', 'name', 'description', 'pack_id', 'format', '_image', 'tags') def __init__(self, *, state, data): self._state = state @@ -76,7 +74,7 @@ class Sticker(Hashable): self.description = data['description'] self.pack_id = int(data['pack_id']) self.format = try_enum(StickerType, data['format_type']) - self.image = data['asset'] + self._image = data['asset'] try: self.tags = [tag.strip() for tag in data['tags'].split(',')] @@ -95,7 +93,7 @@ class Sticker(Hashable): return snowflake_time(self.id) @property - def image_url(self): + def image(self): """Returns an :class:`Asset` for the sticker's image. .. note:: @@ -106,32 +104,7 @@ class Sticker(Hashable): Optional[:class:`Asset`] The resulting CDN asset. """ - return self.image_url_as() - - def image_url_as(self, *, size=1024): - """Optionally returns an :class:`Asset` for the sticker's image. - - The size must be a power of 2 between 16 and 4096. - - .. note:: - This will return ``None`` if the format is ``StickerType.lottie``. - - Parameters - ----------- - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Invalid ``size``. - - Returns - ------- - Optional[:class:`Asset`] - The resulting CDN asset or ``None``. - """ if self.format is StickerType.lottie: return None - return Asset._from_sticker_url(self._state, self, size=size) + return Asset._from_sticker_url(self._state, self.id, self._image) diff --git a/discord/team.py b/discord/team.py index 3278433ab..5e47770b0 100644 --- a/discord/team.py +++ b/discord/team.py @@ -42,8 +42,6 @@ class Team: The team ID. name: :class:`str` The team name - icon: Optional[:class:`str`] - The icon hash, if it exists. owner_id: :class:`int` The team's owner ID. members: List[:class:`TeamMember`] @@ -52,14 +50,14 @@ class Team: .. versionadded:: 1.3 """ - __slots__ = ('_state', 'id', 'name', 'icon', 'owner_id', 'members') + __slots__ = ('_state', 'id', 'name', '_icon', 'owner_id', 'members') def __init__(self, state, data): self._state = state - self.id = utils._get_as_snowflake(data, 'id') + self.id = int(data['id']) self.name = data['name'] - self.icon = data['icon'] + self._icon = data['icon'] self.owner_id = utils._get_as_snowflake(data, 'owner_user_id') self.members = [TeamMember(self, self._state, member) for member in data['members']] @@ -67,40 +65,11 @@ class Team: return f'<{self.__class__.__name__} id={self.id} name={self.name}>' @property - def icon_url(self): - """:class:`.Asset`: Retrieves the team's icon asset. - - This is equivalent to calling :meth:`icon_url_as` with - the default parameters ('webp' format and a size of 1024). - """ - return self.icon_url_as() - - def icon_url_as(self, *, format='webp', size=1024): - """Returns an :class:`Asset` for the icon the team has. - - The format must be one of 'webp', 'jpeg', 'jpg' or 'png'. - The size must be a power of 2 between 16 and 4096. - - .. versionadded:: 2.0 - - Parameters - ----------- - format: :class:`str` - The format to attempt to convert the icon to. Defaults to 'webp'. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_icon(self._state, self, 'team', format=format, size=size) + def icon(self): + """Optional[:class:`.Asset`]: Retrieves the team's icon asset, if any.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path='team') @property def owner(self): diff --git a/discord/user.py b/discord/user.py index 9f55b4ace..888c5c951 100644 --- a/discord/user.py +++ b/discord/user.py @@ -38,7 +38,7 @@ _BaseUser = discord.abc.User class BaseUser(_BaseUser): - __slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', 'system', '_public_flags', '_state') + __slots__ = ('name', 'id', 'discriminator', '_avatar', 'bot', 'system', '_public_flags', '_state') def __init__(self, *, state, data): self._state = state @@ -60,7 +60,7 @@ class BaseUser(_BaseUser): self.name = data['username'] self.id = int(data['id']) self.discriminator = data['discriminator'] - self.avatar = data['avatar'] + self._avatar = data['avatar'] self._public_flags = data.get('public_flags', 0) self.bot = data.get('bot', False) self.system = data.get('system', False) @@ -72,7 +72,7 @@ class BaseUser(_BaseUser): self.name = user.name self.id = user.id self.discriminator = user.discriminator - self.avatar = user.avatar + self._avatar = user._avatar self.bot = user.bot self._state = user._state self._public_flags = user._public_flags @@ -83,7 +83,7 @@ class BaseUser(_BaseUser): return { 'username': self.name, 'id': self.id, - 'avatar': self.avatar, + 'avatar': self._avatar, 'discriminator': self.discriminator, 'bot': self.bot, } @@ -94,66 +94,20 @@ class BaseUser(_BaseUser): return PublicUserFlags._from_value(self._public_flags) @property - def avatar_url(self): + def avatar(self): """:class:`Asset`: Returns an :class:`Asset` for the avatar the user has. If the user does not have a traditional avatar, an asset for the default avatar is returned instead. - - This is equivalent to calling :meth:`avatar_url_as` with - the default parameters (i.e. webp/gif detection and a size of 1024). - """ - return self.avatar_url_as(format=None, size=1024) - - def is_avatar_animated(self): - """:class:`bool`: Indicates if the user has an animated avatar.""" - return bool(self.avatar and self.avatar.startswith('a_')) - - def avatar_url_as(self, *, format=None, static_format='webp', size=1024): - """Returns an :class:`Asset` for the avatar the user has. - - If the user does not have a traditional avatar, an asset for - the default avatar is returned instead. - - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and - 'gif' is only valid for animated avatars. The size must be a power of 2 - between 16 and 4096. - - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the avatar to. - If the format is ``None``, then it is automatically - detected into either 'gif' or static_format depending on the - avatar being animated or not. - static_format: Optional[:class:`str`] - Format to attempt to convert only non-animated avatars to. - Defaults to 'webp' - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or ``static_format``, or - invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. """ - return Asset._from_avatar(self._state, self, format=format, static_format=static_format, size=size) - + if self._avatar is None: + return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar)) + else: + return Asset._from_avatar(self._state, self.id, self._avatar) @property def default_avatar(self): - """:class:`DefaultAvatar`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" - return try_enum(DefaultAvatar, int(self.discriminator) % len(DefaultAvatar)) - - @property - def default_avatar_url(self): - """:class:`Asset`: Returns a URL for a user's default avatar.""" - return Asset(self._state, f'/embed/avatars/{self.default_avatar.value}.png') + """:class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" + return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar)) @property def colour(self): diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index aadc752cd..807eb1a0e 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -475,56 +475,23 @@ class PartialWebhookGuild(Hashable): The partial guild's icon """ - __slots__ = ('id', 'name', 'icon', '_state') + __slots__ = ('id', 'name', '_icon', '_state') def __init__(self, *, data, state): self._state = state self.id = int(data['id']) self.name = data['name'] - self.icon = data['icon'] + self._icon = data['icon'] def __repr__(self): return f'' @property - def icon_url(self) -> Asset: - """:class:`Asset`: Returns the guild's icon asset.""" - return self.icon_url_as() - - def is_icon_animated(self) -> bool: - """:class:`bool`: Returns True if the guild has an animated icon.""" - return bool(self.icon and self.icon.startswith('a_')) - - def icon_url_as(self, *, format=None, static_format='webp', size=1024): - """Returns an :class:`Asset` for the guild's icon. - - The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif', and - 'gif' is only valid for animated avatars. The size must be a power of 2 - between 16 and 4096. - - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the icon to. - If the format is ``None``, then it is automatically - detected into either 'gif' or static_format depending on the - icon being animated or not. - static_format: Optional[:class:`str`] - Format to attempt to convert only non-animated icons to. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. - """ - return Asset._from_guild_icon(self._state, self, format=format, static_format=static_format, size=size) + def icon_url(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the guild's icon asset, if available.""" + if self._icon is None: + return None + return Asset._from_guild_icon(self._state, self.id, self._icon) class _FriendlyHttpAttributeErrorHelper: @@ -684,7 +651,7 @@ class BaseWebhook(Hashable): 'auth_token', 'user', 'name', - 'avatar', + '_avatar', 'source_channel', 'source_guild', '_state', @@ -701,7 +668,7 @@ class BaseWebhook(Hashable): self.channel_id = utils._get_as_snowflake(data, 'channel_id') self.guild_id = utils._get_as_snowflake(data, 'guild_id') self.name = data.get('name') - self.avatar = data.get('avatar') + self._avatar = data.get('avatar') self.token = data.get('token') user = data.get('user') @@ -755,59 +722,16 @@ class BaseWebhook(Hashable): return utils.snowflake_time(self.id) @property - def avatar_url(self) -> Asset: + def avatar(self) -> Asset: """:class:`Asset`: Returns an :class:`Asset` for the avatar the webhook has. If the webhook does not have a traditional avatar, an asset for the default avatar is returned instead. - - This is equivalent to calling :meth:`avatar_url_as` with the - default parameters. - """ - return self.avatar_url_as() - - def avatar_url_as(self, *, format: Optional[Literal['png', 'jpg', 'jpeg']] = None, size: int = 1024) -> Asset: - """Returns an :class:`Asset` for the avatar the webhook has. - - If the webhook does not have a traditional avatar, an asset for - the default avatar is returned instead. - - The format must be one of 'jpeg', 'jpg', or 'png'. - The size must be a power of 2 between 16 and 1024. - - Parameters - ----------- - format: Optional[:class:`str`] - The format to attempt to convert the avatar to. - If the format is ``None``, then it is equivalent to png. - size: :class:`int` - The size of the image to display. - - Raises - ------ - InvalidArgument - Bad image format passed to ``format`` or invalid ``size``. - - Returns - -------- - :class:`Asset` - The resulting CDN asset. """ - if self.avatar is None: + if self._avatar is None: # Default is always blurple apparently - return Asset(self._state, '/embed/avatars/0.png') - - if not utils.valid_icon_size(size): - raise InvalidArgument("size must be a power of 2 between 16 and 1024") - - format = format or 'png' - - if format not in ('png', 'jpg', 'jpeg'): - raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.") - - url = f'/avatars/{self.id}/{self.avatar}.{format}?size={size}' - return Asset(self._state, url) - + return Asset._from_default_avatar(self._state, 0) + return Asset._from_avatar(self._state, self.id, self._avatar) class Webhook(BaseWebhook): """Represents an asynchronous Discord webhook. @@ -980,7 +904,7 @@ class Webhook(BaseWebhook): 'name': name, 'channel_id': channel.id, 'guild_id': channel.guild.id, - 'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user.avatar}, + 'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user._avatar}, } state = channel._state