Redesign asset retrieval in the library.
Most assets now return a new class named `Asset`. This allows for the assets to be consistently saved via a `save` method instead of special casing for `Attachment`. `AppInfo` is no longer a namedtuple it is a fully documented dataclass, as well as having the state attached to it. Fixes #1997
This commit is contained in:
parent
dea3ba5eb7
commit
be227ebcf0
@ -20,7 +20,8 @@ __version__ = '1.0.0a'
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from .client import Client, AppInfo
|
||||
from .client import Client
|
||||
from .appinfo import AppInfo
|
||||
from .user import User, ClientUser, Profile
|
||||
from .emoji import Emoji, PartialEmoji
|
||||
from .activity import *
|
||||
@ -29,6 +30,7 @@ from .guild import Guild
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .message import Message, Attachment
|
||||
from .asset import Asset
|
||||
from .errors import *
|
||||
from .calls import CallMessage, GroupCall
|
||||
from .permissions import Permissions, PermissionOverwrite
|
||||
|
73
discord/appinfo.py
Normal file
73
discord/appinfo.py
Normal file
@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2019 Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .user import User
|
||||
from .asset import Asset
|
||||
|
||||
|
||||
class AppInfo:
|
||||
"""Represents the application info for the bot provided by Discord.
|
||||
|
||||
|
||||
Attributes
|
||||
-------------
|
||||
id: :class:`int`
|
||||
The application ID.
|
||||
name: :class:`str`
|
||||
The application name.
|
||||
owner: :class:`User`
|
||||
The application's owner.
|
||||
icon: Optional[:class:`str`]
|
||||
The icon hash.
|
||||
description: Optional[:class:`str`]
|
||||
The application description.
|
||||
bot_public: :class:`bool`
|
||||
Whether the bot is public.
|
||||
bot_require_code_grant: :class:`bool`
|
||||
Whether the bot requires the completion of the full oauth2 code
|
||||
grant flow to join.
|
||||
rpc_origins: Optional[List[:class:`str`]]
|
||||
A list of RPC origin URLs, if RPC is enabled.
|
||||
"""
|
||||
__slots__ = ('_state', 'description', 'id', 'name', 'rpc_origins',
|
||||
'bot_public', 'bot_require_code_grant', 'owner', 'icon')
|
||||
|
||||
def __init__(self, state, data):
|
||||
self._state = state
|
||||
|
||||
self.id = int(data['id'])
|
||||
self.name = data['name']
|
||||
self.description = data['description']
|
||||
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']
|
||||
self.owner = User(state=self._state, data=data['owner'])
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
""":class:`.Asset`: Retrieves the application's icon asset."""
|
||||
return Asset._from_icon(self._state, self, 'app')
|
157
discord/asset.py
Normal file
157
discord/asset.py
Normal file
@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2019 Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import io
|
||||
from .errors import DiscordException
|
||||
from .errors import InvalidArgument
|
||||
from . import utils
|
||||
|
||||
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
|
||||
class Asset:
|
||||
"""Represents a CDN asset on Discord.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the URL of the CDN asset.
|
||||
|
||||
.. describe:: len(x)
|
||||
|
||||
Returns the length of the CDN asset's URL.
|
||||
|
||||
.. describe:: bool(x)
|
||||
|
||||
Checks if the Asset has a URL.
|
||||
"""
|
||||
__slots__ = ('_state', '_url')
|
||||
|
||||
def __init__(self, state, url=None):
|
||||
self._state = state
|
||||
self._url = url
|
||||
|
||||
@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 1024")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(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("static_format must be one of {}".format(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, 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(user, format, size))
|
||||
|
||||
@classmethod
|
||||
def _from_icon(cls, state, object, path):
|
||||
if object.icon is None:
|
||||
return cls(state)
|
||||
|
||||
url = 'https://cdn.discordapp.com/{0}-icons/{1.id}/{1.icon}.jpg'.format(path, object)
|
||||
return cls(state, url)
|
||||
|
||||
@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("format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if hash is None:
|
||||
return Asset(state)
|
||||
|
||||
url = 'https://cdn.discordapp.com/{key}/{0}/{1}.{2}?size={3}'
|
||||
return cls(state, url.format(id, hash, format, size, key=key))
|
||||
|
||||
def __str__(self):
|
||||
return self._url
|
||||
|
||||
def __len__(self):
|
||||
return len(self._url)
|
||||
|
||||
def __bool__(self):
|
||||
return self._url is not None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Asset url={0._url!r}>'.format(self)
|
||||
|
||||
async def save(self, fp, *, seek_begin=True):
|
||||
"""|coro|
|
||||
|
||||
Saves this asset into a file-like object.
|
||||
|
||||
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 valid URL or internal connection state.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`PartialEmoji` will not have a state if you make
|
||||
your own instance via ``PartialEmoji(animated=False, name='x', id=2345678)``.
|
||||
|
||||
The URL will not be provided if there is no custom image.
|
||||
HTTPException
|
||||
Saving the asset failed.
|
||||
NotFound
|
||||
The asset was deleted.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`int`
|
||||
The number of bytes written.
|
||||
"""
|
||||
if not self._url:
|
||||
raise DiscordException('Invalid asset (no URL provided)')
|
||||
|
||||
if self._state is None:
|
||||
raise DiscordException('Invalid state (no ConnectionState provided)')
|
||||
|
||||
data = await self._state.http.get_from_cdn(self._url)
|
||||
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)
|
@ -32,6 +32,7 @@ from .permissions import Permissions
|
||||
from .enums import ChannelType, try_enum
|
||||
from .mixins import Hashable
|
||||
from . import utils
|
||||
from .asset import Asset
|
||||
from .errors import ClientException, NoMoreItems
|
||||
from .webhook import Webhook
|
||||
|
||||
@ -996,11 +997,8 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
@property
|
||||
def icon_url(self):
|
||||
"""Returns the channel's icon URL if available or an empty string otherwise."""
|
||||
if self.icon is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/channel-icons/{0.id}/{0.icon}.jpg'.format(self)
|
||||
""":class:`Asset`: Returns the channel's icon asset if available."""
|
||||
return Asset._from_icon(self._state, self, 'channel')
|
||||
|
||||
@property
|
||||
def created_at(self):
|
||||
|
@ -35,6 +35,7 @@ import aiohttp
|
||||
import websockets
|
||||
|
||||
from .user import User, Profile
|
||||
from .asset import Asset
|
||||
from .invite import Invite
|
||||
from .widget import Widget
|
||||
from .guild import Guild
|
||||
@ -50,21 +51,10 @@ from . import utils
|
||||
from .backoff import ExponentialBackoff
|
||||
from .webhook import Webhook
|
||||
from .iterators import GuildIterator
|
||||
from .appinfo import AppInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
AppInfo = namedtuple('AppInfo',
|
||||
'id name description rpc_origins bot_public bot_require_code_grant icon owner')
|
||||
|
||||
def app_info_icon_url(self):
|
||||
"""Retrieves the application's icon_url if it exists. Empty string otherwise."""
|
||||
if not self.icon:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/app-icons/{0.id}/{0.icon}.jpg'.format(self)
|
||||
|
||||
AppInfo.icon_url = property(app_info_icon_url)
|
||||
|
||||
class Client:
|
||||
r"""Represents a client connection that connects to Discord.
|
||||
This class is used to interact with the Discord WebSocket and API.
|
||||
@ -1060,11 +1050,7 @@ class Client:
|
||||
data = await self.http.application_info()
|
||||
if 'rpc_origins' not in data:
|
||||
data['rpc_origins'] = None
|
||||
return AppInfo(id=int(data['id']), name=data['name'],
|
||||
description=data['description'], icon=data['icon'],
|
||||
rpc_origins=data['rpc_origins'], bot_public=data['bot_public'],
|
||||
bot_require_code_grant=data['bot_require_code_grant'],
|
||||
owner=User(state=self._connection, data=data['owner']))
|
||||
return AppInfo(self._connection, data)
|
||||
|
||||
async def fetch_user(self, user_id):
|
||||
"""|coro|
|
||||
|
@ -24,11 +24,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from .asset import Asset
|
||||
from . import utils
|
||||
|
||||
class PartialEmoji(namedtuple('PartialEmoji', 'animated name id')):
|
||||
class PartialEmoji:
|
||||
"""Represents a "partial" emoji.
|
||||
|
||||
This model will be given in two scenarios:
|
||||
@ -65,7 +64,19 @@ class PartialEmoji(namedtuple('PartialEmoji', 'animated name id')):
|
||||
The ID of the custom emoji, if applicable.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ('animated', 'name', 'id', '_state')
|
||||
|
||||
def __init__(self, *, animated, name, id=None):
|
||||
self.animated = animated
|
||||
self.name = name
|
||||
self.id = id
|
||||
self._state = None
|
||||
|
||||
@classmethod
|
||||
def with_state(cls, state, *, animated, name, id=None):
|
||||
self = cls(animated=animated, name=name, id=id)
|
||||
self._state = state
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
if self.id is None:
|
||||
@ -81,6 +92,9 @@ class PartialEmoji(namedtuple('PartialEmoji', 'animated name id')):
|
||||
if isinstance(other, (PartialEmoji, Emoji)):
|
||||
return self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.id, self.name))
|
||||
|
||||
@ -99,12 +113,13 @@ class PartialEmoji(namedtuple('PartialEmoji', 'animated name id')):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Returns a URL version of the emoji, if it is custom."""
|
||||
""":class:`Asset`:Returns an asset of the emoji, if it is custom."""
|
||||
if self.is_unicode_emoji():
|
||||
return None
|
||||
return Asset(self._state)
|
||||
|
||||
_format = 'gif' if self.animated else 'png'
|
||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
url = "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
return Asset(self._state, url)
|
||||
|
||||
class Emoji:
|
||||
"""Represents a custom emoji.
|
||||
@ -186,6 +201,9 @@ class Emoji:
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, (PartialEmoji, Emoji)) and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return self.id >> 22
|
||||
|
||||
@ -198,7 +216,8 @@ class Emoji:
|
||||
def url(self):
|
||||
"""Returns a URL version of the emoji."""
|
||||
_format = 'gif' if self.animated else 'png'
|
||||
return "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
url = "https://cdn.discordapp.com/emojis/{0.id}.{1}".format(self, _format)
|
||||
return Asset(self._state, url)
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
|
@ -400,7 +400,8 @@ class PartialEmojiConverter(Converter):
|
||||
emoji_name = match.group(2)
|
||||
emoji_id = int(match.group(3))
|
||||
|
||||
return discord.PartialEmoji(animated=emoji_animated, name=emoji_name, id=emoji_id)
|
||||
return discord.PartialEmoji.with_state(ctx.bot._connection, animated=emoji_animated, name=emoji_name,
|
||||
id=emoji_id)
|
||||
|
||||
raise BadArgument('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
|
||||
|
||||
|
@ -37,14 +37,12 @@ from .errors import InvalidArgument, ClientException
|
||||
from .channel import *
|
||||
from .enums import VoiceRegion, Status, ChannelType, try_enum, VerificationLevel, ContentFilter, NotificationLevel
|
||||
from .mixins import Hashable
|
||||
from .utils import valid_icon_size
|
||||
from .user import User
|
||||
from .invite import Invite
|
||||
from .iterators import AuditLogIterator
|
||||
from .webhook import Webhook
|
||||
from .widget import Widget
|
||||
|
||||
VALID_ICON_FORMATS = {"jpeg", "jpg", "webp", "png"}
|
||||
from .asset import Asset
|
||||
|
||||
BanEntry = namedtuple('BanEntry', 'reason user')
|
||||
|
||||
@ -441,18 +439,10 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The resulting CDN URL.
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.icon is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/icons/{0.id}/{0.icon}.{1}?size={2}'.format(self, format, size)
|
||||
return Asset._from_guild_image(self._state, self.id, self.icon, 'icons', format=format, size=size)
|
||||
|
||||
@property
|
||||
def banner_url(self):
|
||||
@ -479,18 +469,10 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The resulting CDN URL.
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.banner is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/banners/{0.id}/{0.banner}.{1}?size={2}'.format(self, format, size)
|
||||
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
|
||||
|
||||
@property
|
||||
def splash_url(self):
|
||||
@ -517,18 +499,10 @@ class Guild(Hashable):
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The resulting CDN URL.
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.splash is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.{1}?size={2}'.format(self, format, size)
|
||||
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
|
||||
|
||||
@property
|
||||
def member_count(self):
|
||||
|
@ -224,16 +224,16 @@ class HTTPClient:
|
||||
# We've run out of retries, raise.
|
||||
raise HTTPException(r, data)
|
||||
|
||||
async def get_attachment(self, url):
|
||||
async def get_from_cdn(self, url):
|
||||
async with self.__session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.read()
|
||||
elif resp.status == 404:
|
||||
raise NotFound(resp, 'attachment not found')
|
||||
raise NotFound(resp, 'asset not found')
|
||||
elif resp.status == 403:
|
||||
raise Forbidden(resp, 'cannot retrieve attachment')
|
||||
raise Forbidden(resp, 'cannot retrieve asset')
|
||||
else:
|
||||
raise HTTPException(resp, 'failed to get attachment')
|
||||
raise HTTPException(resp, 'failed to get asset')
|
||||
|
||||
# state management
|
||||
|
||||
|
@ -24,14 +24,12 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .utils import parse_time, valid_icon_size, snowflake_time
|
||||
from .asset import Asset
|
||||
from .utils import parse_time, snowflake_time
|
||||
from .mixins import Hashable
|
||||
from .errors import InvalidArgument
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
from collections import namedtuple
|
||||
|
||||
VALID_ICON_FORMATS = {"jpeg", "jpg", "webp", "png"}
|
||||
|
||||
class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
@ -81,7 +79,7 @@ class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
|
||||
"""Returns the channel's creation time in UTC."""
|
||||
return snowflake_time(self.id)
|
||||
|
||||
class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon banner id name splash verification_level description')):
|
||||
class PartialInviteGuild:
|
||||
"""Represents a "partial" invite guild.
|
||||
|
||||
This model will be given when the user is not part of the
|
||||
@ -125,7 +123,19 @@ class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon banner
|
||||
The partial guild's description.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ('_state', 'features', 'icon', 'banner', 'id', 'name', 'splash',
|
||||
'verification_level', 'description')
|
||||
|
||||
def __init__(self, state, data, id):
|
||||
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.verification_level = try_enum(VerificationLevel, data.get('verification_level'))
|
||||
self.description = data.get('description')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -141,16 +151,8 @@ class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon banner
|
||||
return self.icon_url_as()
|
||||
|
||||
def icon_url_as(self, *, format='webp', size=1024):
|
||||
""":class:`str`: The same operation as :meth:`Guild.icon_url_as`."""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.icon is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/icons/{0.id}/{0.icon}.{1}?size={2}'.format(self, format, size)
|
||||
""":class:`Asset`: The same operation as :meth:`Guild.icon_url_as`."""
|
||||
return Asset._from_guild_image(self._state, self.id, self.icon, 'icons', format=format, size=size)
|
||||
|
||||
@property
|
||||
def banner_url(self):
|
||||
@ -158,16 +160,8 @@ class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon banner
|
||||
return self.banner_url_as()
|
||||
|
||||
def banner_url_as(self, *, format='webp', size=2048):
|
||||
""":class:`str`: The same operation as :meth:`Guild.banner_url_as`."""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.banner is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/banners/{0.id}/{0.banner}.{1}?size={2}'.format(self, format, size)
|
||||
""":class:`Asset`: The same operation as :meth:`Guild.banner_url_as`."""
|
||||
return Asset._from_guild_image(self._state, self.id, self.banner, 'banners', format=format, size=size)
|
||||
|
||||
@property
|
||||
def splash_url(self):
|
||||
@ -175,16 +169,8 @@ class PartialInviteGuild(namedtuple('PartialInviteGuild', 'features icon banner
|
||||
return self.splash_url_as()
|
||||
|
||||
def splash_url_as(self, *, format='webp', size=2048):
|
||||
""":class:`str`: The same operation as :meth:`Guild.splash_url_as`."""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
|
||||
if format not in VALID_ICON_FORMATS:
|
||||
raise InvalidArgument("format must be one of {}".format(VALID_ICON_FORMATS))
|
||||
|
||||
if self.splash is None:
|
||||
return ''
|
||||
|
||||
return 'https://cdn.discordapp.com/splashes/{0.id}/{0.splash}.{1}?size={2}'.format(self, format, size)
|
||||
""":class:`Asset`: The same operation as :meth:`Guild.splash_url_as`."""
|
||||
return Asset._from_guild_image(self._state, self.id, self.splash, 'splashes', format=format, size=size)
|
||||
|
||||
class Invite(Hashable):
|
||||
"""Represents a Discord :class:`Guild` or :class:`abc.GuildChannel` invite.
|
||||
@ -240,7 +226,6 @@ class Invite(Hashable):
|
||||
The channel the invite is for.
|
||||
"""
|
||||
|
||||
|
||||
__slots__ = ('max_age', 'code', 'guild', 'revoked', 'created_at', 'uses',
|
||||
'temporary', 'max_uses', 'inviter', 'channel', '_state',
|
||||
'approximate_member_count', 'approximate_presence_count' )
|
||||
@ -274,14 +259,7 @@ class Invite(Hashable):
|
||||
guild_data = data['guild']
|
||||
channel_type = try_enum(ChannelType, channel_data['type'])
|
||||
channel = PartialInviteChannel(id=channel_id, name=channel_data['name'], type=channel_type)
|
||||
guild = PartialInviteGuild(id=guild_id,
|
||||
name=guild_data['name'],
|
||||
features=guild_data.get('features', []),
|
||||
icon=guild_data.get('icon'),
|
||||
banner=guild_data.get('banner'),
|
||||
splash=guild_data.get('splash'),
|
||||
verification_level=try_enum(VerificationLevel, guild_data.get('verification_level')),
|
||||
description=guild_data.get('description'))
|
||||
guild = PartialInviteGuild(state, guild_data, guild_id)
|
||||
data['guild'] = guild
|
||||
data['channel'] = channel
|
||||
return cls(state=state, data=data)
|
||||
|
@ -112,7 +112,7 @@ class Attachment:
|
||||
The number of bytes written.
|
||||
"""
|
||||
url = self.proxy_url if use_cached else self.url
|
||||
data = await self._http.get_attachment(url)
|
||||
data = await self._http.get_from_cdn(url)
|
||||
if isinstance(fp, io.IOBase) and fp.writable():
|
||||
written = fp.write(data)
|
||||
if seek_begin:
|
||||
|
@ -402,7 +402,7 @@ class ConnectionState:
|
||||
def parse_message_reaction_add(self, data):
|
||||
emoji_data = data['emoji']
|
||||
emoji_id = utils._get_as_snowflake(emoji_data, 'id')
|
||||
emoji = PartialEmoji(animated=emoji_data['animated'], id=emoji_id, name=emoji_data['name'])
|
||||
emoji = PartialEmoji.with_state(self, animated=emoji_data['animated'], id=emoji_id, name=emoji_data['name'])
|
||||
raw = RawReactionActionEvent(data, emoji)
|
||||
self.dispatch('raw_reaction_add', raw)
|
||||
|
||||
@ -428,7 +428,7 @@ class ConnectionState:
|
||||
def parse_message_reaction_remove(self, data):
|
||||
emoji_data = data['emoji']
|
||||
emoji_id = utils._get_as_snowflake(emoji_data, 'id')
|
||||
emoji = PartialEmoji(animated=emoji_data['animated'], id=emoji_id, name=emoji_data['name'])
|
||||
emoji = PartialEmoji.with_state(self, animated=emoji_data['animated'], id=emoji_id, name=emoji_data['name'])
|
||||
raw = RawReactionActionEvent(data, emoji)
|
||||
self.dispatch('raw_reaction_remove', raw)
|
||||
|
||||
|
@ -27,13 +27,11 @@ DEALINGS IN THE SOFTWARE.
|
||||
from collections import namedtuple
|
||||
|
||||
import discord.abc
|
||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time, valid_icon_size
|
||||
from .utils import snowflake_time, _bytes_to_base64_data, parse_time
|
||||
from .enums import DefaultAvatar, RelationshipType, UserFlags, HypeSquadHouse, PremiumType, try_enum
|
||||
from .errors import ClientException, InvalidArgument
|
||||
from .errors import ClientException
|
||||
from .colour import Colour
|
||||
|
||||
VALID_STATIC_FORMATS = {"jpeg", "jpg", "webp", "png"}
|
||||
VALID_AVATAR_FORMATS = VALID_STATIC_FORMATS | {"gif"}
|
||||
from .asset import Asset
|
||||
|
||||
class Profile(namedtuple('Profile', 'flags user mutual_guilds connected_accounts premium_since')):
|
||||
__slots__ = ()
|
||||
@ -158,28 +156,10 @@ class BaseUser(_BaseUser):
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The resulting CDN URL.
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if not valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
|
||||
if format is not None and format not in VALID_AVATAR_FORMATS:
|
||||
raise InvalidArgument("format must be None or one of {}".format(VALID_AVATAR_FORMATS))
|
||||
if format == "gif" and not self.is_avatar_animated():
|
||||
raise InvalidArgument("non animated avatars do not support gif format")
|
||||
if static_format not in VALID_STATIC_FORMATS:
|
||||
raise InvalidArgument("static_format must be one of {}".format(VALID_STATIC_FORMATS))
|
||||
|
||||
if self.avatar is None:
|
||||
return self.default_avatar_url
|
||||
|
||||
if format is None:
|
||||
if self.is_avatar_animated():
|
||||
format = 'gif'
|
||||
else:
|
||||
format = static_format
|
||||
|
||||
return 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(self, format, size)
|
||||
return Asset._from_avatar(self._state, self, format=format, static_format=static_format, size=size)
|
||||
|
||||
@property
|
||||
def default_avatar(self):
|
||||
@ -189,7 +169,7 @@ class BaseUser(_BaseUser):
|
||||
@property
|
||||
def default_avatar_url(self):
|
||||
"""Returns a URL for a user's default avatar."""
|
||||
return 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value)
|
||||
return Asset(self._state, 'https://cdn.discordapp.com/embed/avatars/{}.png'.format(self.default_avatar.value))
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
|
@ -34,6 +34,7 @@ import aiohttp
|
||||
from . import utils
|
||||
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound
|
||||
from .user import BaseUser, User
|
||||
from .asset import Asset
|
||||
|
||||
__all__ = ['WebhookAdapter', 'AsyncWebhookAdapter', 'RequestsWebhookAdapter', 'Webhook']
|
||||
|
||||
@ -548,12 +549,12 @@ class Webhook:
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The resulting CDN URL.
|
||||
:class:`Asset`
|
||||
The resulting CDN asset.
|
||||
"""
|
||||
if self.avatar is None:
|
||||
# Default is always blurple apparently
|
||||
return 'https://cdn.discordapp.com/embed/avatars/0.png'
|
||||
return Asset(self._state, 'https://cdn.discordapp.com/embed/avatars/0.png')
|
||||
|
||||
if not utils.valid_icon_size(size):
|
||||
raise InvalidArgument("size must be a power of 2 between 16 and 1024")
|
||||
@ -563,7 +564,8 @@ class Webhook:
|
||||
if format not in ('png', 'jpg', 'jpeg'):
|
||||
raise InvalidArgument("format must be one of 'png', 'jpg', or 'jpeg'.")
|
||||
|
||||
return 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(self, format, size)
|
||||
url = 'https://cdn.discordapp.com/avatars/{0.id}/{0.avatar}.{1}?size={2}'.format(self, format, size)
|
||||
return Asset(self._state, url)
|
||||
|
||||
def delete(self):
|
||||
"""|maybecoro|
|
||||
@ -661,7 +663,7 @@ class Webhook:
|
||||
username: :class:`str`
|
||||
The username to send with this message. If no username is provided
|
||||
then the default username for the webhook is used.
|
||||
avatar_url: :class:`str`
|
||||
avatar_url: Union[:class:`str`, :class:`Asset`]
|
||||
The avatar URL to send with this message. If no avatar URL is provided
|
||||
then the default avatar for the webhook is used.
|
||||
tts: :class:`bool`
|
||||
@ -716,7 +718,7 @@ class Webhook:
|
||||
|
||||
payload['tts'] = tts
|
||||
if avatar_url:
|
||||
payload['avatar_url'] = avatar_url
|
||||
payload['avatar_url'] = str(avatar_url)
|
||||
if username:
|
||||
payload['username'] = username
|
||||
|
||||
|
@ -31,8 +31,6 @@ from .invite import Invite
|
||||
from .enums import Status, try_enum
|
||||
from collections import namedtuple
|
||||
|
||||
VALID_ICON_FORMATS = {"jpeg", "jpg", "webp", "png"}
|
||||
|
||||
class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
|
||||
"""Represents a "partial" widget channel.
|
||||
|
||||
|
@ -40,6 +40,9 @@ Client
|
||||
.. autoclass:: AutoShardedClient
|
||||
:members:
|
||||
|
||||
.. autoclass:: AppInfo
|
||||
:members:
|
||||
|
||||
Voice
|
||||
------
|
||||
|
||||
@ -1918,6 +1921,12 @@ Attachment
|
||||
.. autoclass:: Attachment()
|
||||
:members:
|
||||
|
||||
Asset
|
||||
~~~~~
|
||||
|
||||
.. autoclass:: Asset()
|
||||
:members:
|
||||
|
||||
Message
|
||||
~~~~~~~
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user