Browse Source

Rewrite Asset design

This is a breaking change.

This does the following transformations, assuming `asset` represents
an asset type.

Object.is_asset_animated() => Object.asset.is_animated()
Object.asset => Object.asset.key
Object.asset_url => Object.asset_url
Object.asset_url_as => Object.asset.replace(...)

Since the asset type now requires a key (or hash, if you will),
Emoji had to be flattened similar to how Attachment was done since
these assets are keyed solely ID.

Emoji.url (Asset) => Emoji.url (str)
Emoji.url_as => removed
Emoji.url.read => Emoji.read
Emoji.url.save => Emoji.save

This transformation was also done to PartialEmoji.
pull/6744/head
Rapptz 4 years ago
parent
commit
9eaf1e85e4
  1. 98
      discord/appinfo.py
  2. 339
      discord/asset.py
  3. 57
      discord/channel.py
  4. 104
      discord/emoji.py
  5. 2
      discord/flags.py
  6. 179
      discord/guild.py
  7. 75
      discord/invite.py
  8. 6
      discord/member.py
  9. 4
      discord/message.py
  10. 96
      discord/partial_emoji.py
  11. 35
      discord/sticker.py
  12. 47
      discord/team.py
  13. 68
      discord/user.py
  14. 104
      discord/webhook/async_.py

98
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):

339
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'<Asset url={self._url!r}>'
shorten = self._url.replace(self.BASE, '')
return f'<Asset url={shorten!r}>'
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

57
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):

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

2
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 <need_members_intent>`.

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

75
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):

6
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.

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

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

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

47
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):

68
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):

104
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'<PartialWebhookGuild name={self.name!r} id={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."""
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

Loading…
Cancel
Save