conflict fixes

This commit is contained in:
iDutchy 2021-01-14 18:03:09 -06:00
commit 86fd3fb738
39 changed files with 1181 additions and 184 deletions

View File

@ -62,6 +62,6 @@ from .sticker import Sticker
VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial') VersionInfo = namedtuple('VersionInfo', 'major minor micro enhanced releaselevel serial')
version_info = VersionInfo(major=1, minor=6, micro=0, enhanced=6, releaselevel='alpha', serial=0) version_info = VersionInfo(major=1, minor=6, micro=0, enhanced=7, releaselevel='alpha', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@ -758,7 +758,7 @@ class GuildChannel:
Returns a list of all active instant invites from this channel. Returns a list of all active instant invites from this channel.
You must have :attr:`~Permissions.manage_guild` to get this information. You must have :attr:`~Permissions.manage_channels` to get this information.
Raises Raises
------- -------

View File

@ -153,6 +153,19 @@ class Asset:
return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size)) return cls(state, '/stickers/{0.id}/{0.image}.png?size={2}'.format(sticker, format, size))
@classmethod
def _from_emoji(cls, state, emoji, *, format=None, static_format='png'):
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 emoji.animated:
raise InvalidArgument("non animated emoji's 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 format is None:
format = 'gif' if emoji.animated else static_format
return cls(state, '/emojis/{0.id}.{1}'.format(emoji, format))
def __str__(self): def __str__(self):
return self.BASE + self._url if self._url is not None else '' return self.BASE + self._url if self._url is not None else ''

Binary file not shown.

Binary file not shown.

View File

@ -34,7 +34,6 @@ from .mixins import Hashable
from . import utils from . import utils
from .asset import Asset from .asset import Asset
from .errors import ClientException, NoMoreItems, InvalidArgument from .errors import ClientException, NoMoreItems, InvalidArgument
from .webhook import Webhook
__all__ = ( __all__ = (
'TextChannel', 'TextChannel',
@ -228,7 +227,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
A value of `0` disables slowmode. The maximum value possible is `21600`. A value of `0` disables slowmode. The maximum value possible is `21600`.
type: :class:`ChannelType` type: :class:`ChannelType`
Change the type of this text channel. Currently, only conversion between Change the type of this text channel. Currently, only conversion between
:attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This
is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`.
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for editing this channel. Shows up on the audit log. The reason for editing this channel. Shows up on the audit log.
@ -436,6 +435,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
The webhooks for this channel. The webhooks for this channel.
""" """
from .webhook import Webhook
data = await self._state.http.channel_webhooks(self.id) data = await self._state.http.channel_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data] return [Webhook.from_state(d, state=self._state) for d in data]
@ -472,6 +472,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
The created webhook. The created webhook.
""" """
from .webhook import Webhook
if avatar is not None: if avatar is not None:
avatar = utils._bytes_to_base64_data(avatar) avatar = utils._bytes_to_base64_data(avatar)
@ -519,9 +520,32 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
if not isinstance(destination, TextChannel): if not isinstance(destination, TextChannel):
raise InvalidArgument('Expected TextChannel received {0.__name__}'.format(type(destination))) raise InvalidArgument('Expected TextChannel received {0.__name__}'.format(type(destination)))
from .webhook import Webhook
data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason) data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason)
return Webhook._as_follower(data, channel=destination, user=self._state.user) return Webhook._as_follower(data, channel=destination, user=self._state.user)
def get_partial_message(self, message_id):
"""Creates a :class:`PartialMessage` from the message ID.
This is useful if you want to work with a message and only have its ID without
doing an unnecessary API call.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to create a partial message for.
Returns
---------
:class:`PartialMessage`
The partial message.
"""
from .message import PartialMessage
return PartialMessage(channel=self, id=message_id)
class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
"""Represents a Discord guild voice channel. """Represents a Discord guild voice channel.
@ -1079,6 +1103,28 @@ class DMChannel(discord.abc.Messageable, Hashable):
base.manage_messages = False base.manage_messages = False
return base return base
def get_partial_message(self, message_id):
"""Creates a :class:`PartialMessage` from the message ID.
This is useful if you want to work with a message and only have its ID without
doing an unnecessary API call.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to create a partial message for.
Returns
---------
:class:`PartialMessage`
The partial message.
"""
from .message import PartialMessage
return PartialMessage(channel=self, id=message_id)
class GroupChannel(discord.abc.Messageable, Hashable): class GroupChannel(discord.abc.Messageable, Hashable):
"""Represents a Discord group channel. """Represents a Discord group channel.

View File

@ -134,10 +134,12 @@ class Emoji(_EmojiTag):
@property @property
def url(self): def url(self):
""":class:`Asset`: Returns the asset of the emoji.""" """:class:`Asset`: Returns the asset of the emoji.
_format = 'gif' if self.animated else 'png'
url = "/emojis/{0.id}.{1}".format(self, _format) This is equivalent to calling :meth:`url_as` with
return Asset(self._state, url) the default parameters (i.e. png/gif detection).
"""
return self.url_as(format=None)
@property @property
def roles(self): def roles(self):
@ -156,6 +158,39 @@ class Emoji(_EmojiTag):
""":class:`Guild`: The guild this emoji belongs to.""" """:class:`Guild`: The guild this emoji belongs to."""
return self._state._get_guild(self.guild_id) 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): def is_usable(self):
""":class:`bool`: Whether the bot can use this emoji. """:class:`bool`: Whether the bot can use this emoji.

View File

@ -263,7 +263,7 @@ class MessageConverter(Converter):
3. Lookup by message URL 3. Lookup by message URL
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound`, `MessageNotFound` or `ChannelNotReadable` instead of generic :exc:`.BadArgument` Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
""" """
async def convert(self, ctx, argument): async def convert(self, ctx, argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$') id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$')

View File

@ -144,7 +144,7 @@ class Command(_BaseCommand):
The long help text for the command. The long help text for the command.
brief: Optional[:class:`str`] brief: Optional[:class:`str`]
The short help text for the command. The short help text for the command.
usage: :class:`str` usage: Optional[:class:`str`]
A replacement for arguments in the default help text. A replacement for arguments in the default help text.
aliases: Union[List[:class:`str`], Tuple[:class:`str`]] aliases: Union[List[:class:`str`], Tuple[:class:`str`]]
The list of aliases the command can be invoked under. The list of aliases the command can be invoked under.
@ -778,17 +778,22 @@ class Command(_BaseCommand):
if not await self.can_run(ctx): if not await self.can_run(ctx):
raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self)) raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self))
if self.cooldown_after_parsing:
await self._parse_arguments(ctx)
self._prepare_cooldowns(ctx)
else:
self._prepare_cooldowns(ctx)
await self._parse_arguments(ctx)
if self._max_concurrency is not None: if self._max_concurrency is not None:
await self._max_concurrency.acquire(ctx) await self._max_concurrency.acquire(ctx)
await self.call_before_hooks(ctx) try:
if self.cooldown_after_parsing:
await self._parse_arguments(ctx)
self._prepare_cooldowns(ctx)
else:
self._prepare_cooldowns(ctx)
await self._parse_arguments(ctx)
await self.call_before_hooks(ctx)
except:
if self._max_concurrency is not None:
await self._max_concurrency.release(ctx)
raise
def is_on_cooldown(self, ctx): def is_on_cooldown(self, ctx):
"""Checks whether the command is currently on cooldown. """Checks whether the command is currently on cooldown.
@ -1140,6 +1145,7 @@ class GroupMixin:
self.all_commands[command.name] = command self.all_commands[command.name] = command
for alias in command.aliases: for alias in command.aliases:
if alias in self.all_commands: if alias in self.all_commands:
self.remove_command(command.name)
raise CommandRegistrationError(alias, alias_conflict=True) raise CommandRegistrationError(alias, alias_conflict=True)
self.all_commands[alias] = command self.all_commands[alias] = command
@ -1172,7 +1178,12 @@ class GroupMixin:
# we're not removing the alias so let's delete the rest of them. # we're not removing the alias so let's delete the rest of them.
for alias in command.aliases: for alias in command.aliases:
self.all_commands.pop(alias, None) cmd = self.all_commands.pop(alias, None)
# in the case of a CommandRegistrationError, an alias might conflict
# with an already existing command. If this is the case, we want to
# make sure the pre-existing command is not removed.
if cmd not in (None, command):
self.all_commands[alias] = cmd
return command return command
def walk_commands(self): def walk_commands(self):

View File

@ -289,7 +289,7 @@ class ChannelNotFound(BadArgument):
Attributes Attributes
----------- -----------
channel: :class:`str` argument: :class:`str`
The channel supplied by the caller that was not found The channel supplied by the caller that was not found
""" """
def __init__(self, argument): def __init__(self, argument):

View File

@ -160,6 +160,26 @@ class Loop:
return None return None
return self._next_iteration return self._next_iteration
async def __call__(self, *args, **kwargs):
"""|coro|
Calls the internal callback that the task holds.
.. versionadded:: 1.6
Parameters
------------
\*args
The arguments to use.
\*\*kwargs
The keyword arguments to use.
"""
if self._injected is not None:
args = (self._injected, *args)
return await self.coro(*args, **kwargs)
def start(self, *args, **kwargs): def start(self, *args, **kwargs):
r"""Starts the internal task in the event loop. r"""Starts the internal task in the event loop.

View File

@ -28,7 +28,7 @@ import os.path
import io import io
class File: class File:
"""A parameter object used for :meth:`abc.Messageable.send` r"""A parameter object used for :meth:`abc.Messageable.send`
for sending file objects. for sending file objects.
.. note:: .. note::

View File

@ -636,11 +636,12 @@ class DiscordWebSocket:
} }
await self.send_as_json(payload) await self.send_as_json(payload)
async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, nonce=None): async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None):
payload = { payload = {
'op': self.REQUEST_MEMBERS, 'op': self.REQUEST_MEMBERS,
'd': { 'd': {
'guild_id': guild_id, 'guild_id': guild_id,
'presences': presences,
'limit': limit 'limit': limit
} }
} }

View File

@ -41,7 +41,6 @@ from .mixins import Hashable
from .user import User from .user import User
from .invite import Invite from .invite import Invite
from .iterators import AuditLogIterator, MemberIterator from .iterators import AuditLogIterator, MemberIterator
from .webhook import Webhook
from .widget import Widget from .widget import Widget
from .asset import Asset from .asset import Asset
from .flags import SystemChannelFlags from .flags import SystemChannelFlags
@ -145,6 +144,8 @@ class Guild(Hashable):
- ``ANIMATED_ICON``: Guild can upload an animated icon. - ``ANIMATED_ICON``: Guild can upload an animated icon.
- ``PUBLIC_DISABLED``: Guild cannot be public. - ``PUBLIC_DISABLED``: Guild cannot be public.
- ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen - ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen
- ``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`] splash: Optional[:class:`str`]
The guild's invite splash. The guild's invite splash.
@ -485,7 +486,7 @@ class Guild(Hashable):
@property @property
def public_updates_channel(self): def public_updates_channel(self):
"""Optional[:class:`TextChannel`]: Return's the guild's channel where admins and """Optional[:class:`TextChannel`]: Return's the guild's channel where admins and
moderators of the guilds receive notices from Discord. The guild must be a moderators of the guilds receive notices from Discord. The guild must be a
Community guild. Community guild.
If no channel is set, then this returns ``None``. If no channel is set, then this returns ``None``.
@ -1100,6 +1101,9 @@ class Guild(Hashable):
The new channel that is used for the system channel. Could be ``None`` for no system channel. The new channel that is used for the system channel. Could be ``None`` for no system channel.
system_channel_flags: :class:`SystemChannelFlags` system_channel_flags: :class:`SystemChannelFlags`
The new system channel settings to use with the new system channel. The new system channel settings to use with the new system channel.
preferred_locale: :class:`str`
The new preferred locale for the guild. Used as the primary language in the guild.
If set, this must be an ISO 639 code, e.g. ``en-US`` or ``ja`` or ``zh-CN``.
rules_channel: Optional[:class:`TextChannel`] rules_channel: Optional[:class:`TextChannel`]
The new channel that is used for rules. This is only available to The new channel that is used for rules. This is only available to
guilds that contain ``PUBLIC`` in :attr:`Guild.features`. Could be ``None`` for no rules guilds that contain ``PUBLIC`` in :attr:`Guild.features`. Could be ``None`` for no rules
@ -1535,6 +1539,7 @@ class Guild(Hashable):
The webhooks for this guild. The webhooks for this guild.
""" """
from .webhook import Webhook
data = await self._state.http.guild_webhooks(self.id) data = await self._state.http.guild_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data] return [Webhook.from_state(d, state=self._state) for d in data]
@ -1787,7 +1792,7 @@ class Guild(Hashable):
The role name. Defaults to 'new role'. The role name. Defaults to 'new role'.
permissions: :class:`Permissions` permissions: :class:`Permissions`
The permissions to have. Defaults to no permissions. The permissions to have. Defaults to no permissions.
colour: :class:`Colour` colour: Union[:class:`Colour`, :class:`int`]
The colour for the role. Defaults to :meth:`Colour.default`. The colour for the role. Defaults to :meth:`Colour.default`.
This is aliased to ``color`` as well. This is aliased to ``color`` as well.
hoist: :class:`bool` hoist: :class:`bool`
@ -1826,6 +1831,8 @@ class Guild(Hashable):
except KeyError: except KeyError:
colour = fields.get('color', Colour.default()) colour = fields.get('color', Colour.default())
finally: finally:
if isinstance(colour, int):
colour = Colour(value=colour)
fields['color'] = colour.value fields['color'] = colour.value
valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable')
@ -2157,7 +2164,7 @@ class Guild(Hashable):
return await self._state.chunk_guild(self, cache=cache) return await self._state.chunk_guild(self, cache=cache)
async def query_members(self, query=None, *, limit=5, user_ids=None, cache=True): async def query_members(self, query=None, *, limit=5, user_ids=None, presences=False, cache=True):
"""|coro| """|coro|
Request members that belong to this guild whose username starts with Request members that belong to this guild whose username starts with
@ -2174,6 +2181,12 @@ class Guild(Hashable):
limit: :class:`int` limit: :class:`int`
The maximum number of members to send back. This must be The maximum number of members to send back. This must be
a number between 5 and 100. a number between 5 and 100.
presences: :class:`bool`
Whether to request for presences to be provided. This defaults
to ``False``.
.. versionadded:: 1.6
cache: :class:`bool` cache: :class:`bool`
Whether to cache the members internally. This makes operations Whether to cache the members internally. This makes operations
such as :meth:`get_member` work for those that matched. such as :meth:`get_member` work for those that matched.
@ -2189,6 +2202,8 @@ class Guild(Hashable):
The query timed out waiting for the members. The query timed out waiting for the members.
ValueError ValueError
Invalid parameters were passed to the function Invalid parameters were passed to the function
ClientException
The presences intent is not enabled.
Returns Returns
-------- --------
@ -2196,6 +2211,9 @@ class Guild(Hashable):
The list of members that have matched the query. The list of members that have matched the query.
""" """
if presences and not self._state._intents.presences:
raise ClientException('Intents.presences must be enabled to use this.')
if query is None: if query is None:
if query == '': if query == '':
raise ValueError('Cannot pass empty query string.') raise ValueError('Cannot pass empty query string.')
@ -2207,7 +2225,7 @@ class Guild(Hashable):
raise ValueError('Cannot pass both query and user_ids') raise ValueError('Cannot pass both query and user_ids')
limit = min(100, limit or 5) limit = min(100, limit or 5)
return await self._state.query_members(self, query=query, limit=limit, user_ids=user_ids, cache=cache) return await self._state.query_members(self, query=query, limit=limit, user_ids=user_ids, presences=presences, cache=cache)
async def change_voice_state(self, *, channel, self_mute=False, self_deaf=False): async def change_voice_state(self, *, channel, self_mute=False, self_deaf=False):
"""|coro| """|coro|

View File

@ -659,7 +659,7 @@ class HTTPClient:
'system_channel_id', 'default_message_notifications', 'system_channel_id', 'default_message_notifications',
'description', 'explicit_content_filter', 'banner', 'description', 'explicit_content_filter', 'banner',
'system_channel_flags', 'rules_channel_id', 'system_channel_flags', 'rules_channel_id',
'public_updates_channel_id') 'public_updates_channel_id', 'preferred_locale',)
payload = { payload = {
k: v for k, v in fields.items() if k in valid_keys k: v for k, v in fields.items() if k in valid_keys

View File

@ -157,13 +157,17 @@ class Member(discord.abc.Messageable, _BaseUser):
The guild that the member belongs to. The guild that the member belongs to.
nick: Optional[:class:`str`] nick: Optional[:class:`str`]
The guild specific nickname of the user. The guild specific nickname of the user.
pending: :class:`bool`
Whether the member is pending member verification.
.. versionadded:: 1.6
premium_since: Optional[:class:`datetime.datetime`] premium_since: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC when the member used their A datetime object that specifies the date and time in UTC when the member used their
Nitro boost on the guild, if available. This could be ``None``. Nitro boost on the guild, if available. This could be ``None``.
""" """
__slots__ = ('_roles', 'joined_at', 'premium_since', '_client_status', __slots__ = ('_roles', 'joined_at', 'premium_since', '_client_status',
'activities', 'guild', 'nick', '_user', '_state') 'activities', 'guild', 'pending', 'nick', '_user', '_state')
def __init__(self, *, data, guild, state): def __init__(self, *, data, guild, state):
self._state = state self._state = state
@ -177,6 +181,7 @@ class Member(discord.abc.Messageable, _BaseUser):
} }
self.activities = tuple(map(create_activity, data.get('activities', []))) self.activities = tuple(map(create_activity, data.get('activities', [])))
self.nick = data.get('nick', None) self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
def __str__(self): def __str__(self):
return str(self._user) return str(self._user)
@ -208,6 +213,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self.premium_since = utils.parse_time(data.get('premium_since')) self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data) self._update_roles(data)
self.nick = data.get('nick', None) self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
@classmethod @classmethod
def _try_upgrade(cls, *, data, guild, state): def _try_upgrade(cls, *, data, guild, state):
@ -241,6 +247,7 @@ class Member(discord.abc.Messageable, _BaseUser):
self._client_status = member._client_status.copy() self._client_status = member._client_status.copy()
self.guild = member.guild self.guild = member.guild
self.nick = member.nick self.nick = member.nick
self.pending = member.pending
self.activities = member.activities self.activities = member.activities
self._state = member._state self._state = member._state
@ -264,6 +271,11 @@ class Member(discord.abc.Messageable, _BaseUser):
except KeyError: except KeyError:
pass pass
try:
self.pending = data['pending']
except KeyError:
pass
self.premium_since = utils.parse_time(data.get('premium_since')) self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data) self._update_roles(data)
@ -634,7 +646,8 @@ class Member(discord.abc.Messageable, _BaseUser):
Gives the member a number of :class:`Role`\s. Gives the member a number of :class:`Role`\s.
You must have the :attr:`~Permissions.manage_roles` permission to You must have the :attr:`~Permissions.manage_roles` permission to
use this. use this, and the added :class:`Role`\s must appear lower in the list
of roles than the highest role of the member.
Parameters Parameters
----------- -----------
@ -672,7 +685,8 @@ class Member(discord.abc.Messageable, _BaseUser):
Removes :class:`Role`\s from this member. Removes :class:`Role`\s from this member.
You must have the :attr:`~Permissions.manage_roles` permission to You must have the :attr:`~Permissions.manage_roles` permission to
use this. use this, and the removed :class:`Role`\s must appear lower in the list
of roles than the highest role of the member.
Parameters Parameters
----------- -----------

View File

@ -34,7 +34,7 @@ from .reaction import Reaction
from .emoji import Emoji from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .calls import CallMessage from .calls import CallMessage
from .enums import MessageType, try_enum from .enums import MessageType, ChannelType, try_enum
from .errors import InvalidArgument, ClientException, HTTPException from .errors import InvalidArgument, ClientException, HTTPException
from .embeds import Embed from .embeds import Embed
from .member import Member from .member import Member
@ -48,10 +48,26 @@ from .sticker import Sticker
__all__ = ( __all__ = (
'Attachment', 'Attachment',
'Message', 'Message',
'PartialMessage',
'MessageReference', 'MessageReference',
'DeletedReferencedMessage', 'DeletedReferencedMessage',
) )
def convert_emoji_reaction(emoji):
if isinstance(emoji, Reaction):
emoji = emoji.emoji
if isinstance(emoji, Emoji):
return '%s:%s' % (emoji.name, emoji.id)
if isinstance(emoji, PartialEmoji):
return emoji._as_reaction()
if isinstance(emoji, str):
# Reactions can be in :name:id format, but not <:name:id>.
# No existing emojis have <> in them, so this should be okay.
return emoji.strip('<>')
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
class Attachment: class Attachment:
"""Represents an attachment from Discord. """Represents an attachment from Discord.
@ -268,7 +284,7 @@ class MessageReference:
The guild id of the message referenced. The guild id of the message referenced.
resolved: Optional[Union[:class:`Message`, :class:`DeletedReferencedMessage`]] resolved: Optional[Union[:class:`Message`, :class:`DeletedReferencedMessage`]]
The message that this reference resolved to. If this is ``None`` The message that this reference resolved to. If this is ``None``
then the original message was not fetched either due to the discord API then the original message was not fetched either due to the Discord API
not attempting to resolve it or it not being available at the time of creation. not attempting to resolve it or it not being available at the time of creation.
If the message was resolved at a prior point but has since been deleted then If the message was resolved at a prior point but has since been deleted then
this will be of type :class:`DeletedReferencedMessage`. this will be of type :class:`DeletedReferencedMessage`.
@ -372,7 +388,19 @@ def flatten_handlers(cls):
class Message(Hashable): class Message(Hashable):
r"""Represents a message from Discord. r"""Represents a message from Discord.
There should be no need to create one of these manually. .. container:: operations
.. describe:: x == y
Checks if two messages are equal.
.. describe:: x != y
Checks if two messages are not equal.
.. describe:: hash(x)
Returns the message's hash.
Attributes Attributes
----------- -----------
@ -423,7 +451,7 @@ class Message(Hashable):
.. warning:: .. warning::
The order of the mentions list is not in any particular order so you should The order of the mentions list is not in any particular order so you should
not rely on it. This is a discord limitation, not one with the library. not rely on it. This is a Discord limitation, not one with the library.
channel_mentions: List[:class:`abc.GuildChannel`] channel_mentions: List[:class:`abc.GuildChannel`]
A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message
then the list is always empty. then the list is always empty.
@ -996,14 +1024,6 @@ class Message(Hashable):
are used instead. are used instead.
.. versionadded:: 1.4 .. versionadded:: 1.4
.. versionchanged:: 1.6
:attr:`~discord.Client.allowed_mentions` serves as defaults unconditionally.
mention_author: Optional[:class:`bool`]
Overrides the :attr:`~discord.AllowedMentions.replied_user` attribute
of ``allowed_mentions``.
.. versionadded:: 1.6
Raises Raises
------- -------
@ -1041,24 +1061,17 @@ class Message(Hashable):
delete_after = fields.pop('delete_after', None) delete_after = fields.pop('delete_after', None)
mention_author = fields.pop('mention_author', None) try:
allowed_mentions = fields.pop('allowed_mentions', None) allowed_mentions = fields.pop('allowed_mentions')
if allowed_mentions is not None: except KeyError:
if self._state.allowed_mentions is not None: pass
allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions) else:
allowed_mentions = allowed_mentions.to_dict() if allowed_mentions is not None:
if mention_author is not None: if self._state.allowed_mentions is not None:
allowed_mentions['replied_user'] = mention_author allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions).to_dict()
fields['allowed_mentions'] = allowed_mentions else:
elif mention_author is not None: allowed_mentions = allowed_mentions.to_dict()
if self._state.allowed_mentions is not None: fields['allowed_mentions'] = allowed_mentions
allowed_mentions = self._state.allowed_mentions.to_dict()
allowed_mentions['replied_user'] = mention_author
else:
allowed_mentions = {'replied_user': mention_author}
fields['allowed_mentions'] = allowed_mentions
elif self._state.allowed_mentions is not None:
fields['allowed_mentions'] = self._state.allowed_mentions.to_dict()
if fields: if fields:
data = await self._state.http.edit_message(self.channel.id, self.id, **fields) data = await self._state.http.edit_message(self.channel.id, self.id, **fields)
@ -1170,7 +1183,7 @@ class Message(Hashable):
The emoji parameter is invalid. The emoji parameter is invalid.
""" """
emoji = self._emoji_reaction(emoji) emoji = convert_emoji_reaction(emoji)
await self._state.http.add_reaction(self.channel.id, self.id, emoji) await self._state.http.add_reaction(self.channel.id, self.id, emoji)
async def remove_reaction(self, emoji, member): async def remove_reaction(self, emoji, member):
@ -1205,7 +1218,7 @@ class Message(Hashable):
The emoji parameter is invalid. The emoji parameter is invalid.
""" """
emoji = self._emoji_reaction(emoji) emoji = convert_emoji_reaction(emoji)
if member.id == self._state.self_id: if member.id == self._state.self_id:
await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji) await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji)
@ -1240,25 +1253,9 @@ class Message(Hashable):
The emoji parameter is invalid. The emoji parameter is invalid.
""" """
emoji = self._emoji_reaction(emoji) emoji = convert_emoji_reaction(emoji)
await self._state.http.clear_single_reaction(self.channel.id, self.id, emoji) await self._state.http.clear_single_reaction(self.channel.id, self.id, emoji)
@staticmethod
def _emoji_reaction(emoji):
if isinstance(emoji, Reaction):
emoji = emoji.emoji
if isinstance(emoji, Emoji):
return '%s:%s' % (emoji.name, emoji.id)
if isinstance(emoji, PartialEmoji):
return emoji._as_reaction()
if isinstance(emoji, str):
# Reactions can be in :name:id format, but not <:name:id>.
# No existing emojis have <> in them, so this should be okay.
return emoji.strip('<>')
raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji))
async def clear_reactions(self): async def clear_reactions(self):
"""|coro| """|coro|
@ -1301,7 +1298,7 @@ class Message(Hashable):
A shortcut method to :meth:`abc.Messageable.send` to reply to the A shortcut method to :meth:`abc.Messageable.send` to reply to the
:class:`Message`. :class:`Message`.
.. versionadded:: 1.6 .. versionadded:: 1.6
Raises Raises
-------- --------
@ -1344,3 +1341,116 @@ class Message(Hashable):
data['guild_id'] = self.guild.id data['guild_id'] = self.guild.id
return data return data
def implement_partial_methods(cls):
msg = Message
for name in cls._exported_names:
func = getattr(msg, name)
setattr(cls, name, func)
return cls
@implement_partial_methods
class PartialMessage(Hashable):
"""Represents a partial message to aid with working messages when only
a message and channel ID are present.
There are two ways to construct this class. The first one is through
the constructor itself, and the second is via
:meth:`TextChannel.get_partial_message` or :meth:`DMChannel.get_partial_message`.
Note that this class is trimmed down and has no rich attributes.
.. versionadded:: 1.6
.. container:: operations
.. describe:: x == y
Checks if two partial messages are equal.
.. describe:: x != y
Checks if two partial messages are not equal.
.. describe:: hash(x)
Returns the partial message's hash.
Attributes
-----------
channel: Union[:class:`TextChannel`, :class:`DMChannel`]
The channel associated with this partial message.
id: :class:`int`
The message ID.
"""
__slots__ = ('channel', 'id', '_cs_guild', '_state')
_exported_names = (
'jump_url',
'delete',
'edit',
'publish',
'pin',
'unpin',
'add_reaction',
'remove_reaction',
'clear_reaction',
'clear_reactions',
'reply',
'to_reference',
'to_message_reference_dict',
)
def __init__(self, *, channel, id):
if channel.type not in (ChannelType.text, ChannelType.news, ChannelType.private):
raise TypeError('Expected TextChannel or DMChannel not %r' % type(channel))
self.channel = channel
self._state = channel._state
self.id = id
def _update(self, data):
# This is used for duck typing purposes.
# Just do nothing with the data.
pass
# Also needed for duck typing purposes
# n.b. not exposed
pinned = property(None, lambda x, y: ...)
def __repr__(self):
return '<PartialMessage id={0.id} channel={0.channel!r}>'.format(self)
@property
def created_at(self):
""":class:`datetime.datetime`: The partial message's creation time in UTC."""
return utils.snowflake_time(self.id)
@utils.cached_slot_property('_cs_guild')
def guild(self):
"""Optional[:class:`Guild`]: The guild that the partial message belongs to, if applicable."""
return getattr(self.channel, 'guild', None)
async def fetch(self):
"""|coro|
Fetches the partial message to a full :class:`Message`.
Raises
--------
NotFound
The message was not found.
Forbidden
You do not have the permissions required to get a message.
HTTPException
Retrieving the message failed.
Returns
--------
:class:`Message`
The full message.
"""
data = await self._state.http.get_message(self.channel.id, self.id)
return self._state.create_message(channel=self.channel, data=data)

View File

@ -28,13 +28,16 @@ import array
import ctypes import ctypes
import ctypes.util import ctypes.util
import logging import logging
import math
import os.path import os.path
import struct
import sys import sys
from .errors import DiscordException from .errors import DiscordException
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int_ptr = ctypes.POINTER(ctypes.c_int)
c_int16_ptr = ctypes.POINTER(ctypes.c_int16) c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
c_float_ptr = ctypes.POINTER(ctypes.c_float) c_float_ptr = ctypes.POINTER(ctypes.c_float)
@ -43,17 +46,55 @@ _lib = None
class EncoderStruct(ctypes.Structure): class EncoderStruct(ctypes.Structure):
pass pass
class DecoderStruct(ctypes.Structure):
pass
EncoderStructPtr = ctypes.POINTER(EncoderStruct) EncoderStructPtr = ctypes.POINTER(EncoderStruct)
DecoderStructPtr = ctypes.POINTER(DecoderStruct)
## Some constants from opus_defines.h
# Error codes
OK = 0
BAD_ARG = -1
# Encoder CTLs
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
APPLICATION_LOWDELAY = 2051
CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
CTL_SET_FEC = 4012
CTL_SET_PLP = 4014
CTL_SET_SIGNAL = 4024
# Decoder CTLs
CTL_SET_GAIN = 4034
CTL_LAST_PACKET_DURATION = 4039
band_ctl = {
'narrow': 1101,
'medium': 1102,
'wide': 1103,
'superwide': 1104,
'full': 1105,
}
signal_ctl = {
'auto': -1000,
'voice': 3001,
'music': 3002,
}
def _err_lt(result, func, args): def _err_lt(result, func, args):
if result < 0: if result < OK:
log.info('error has happened in %s', func.__name__) log.info('error has happened in %s', func.__name__)
raise OpusError(result) raise OpusError(result)
return result return result
def _err_ne(result, func, args): def _err_ne(result, func, args):
ret = args[-1]._obj ret = args[-1]._obj
if ret.value != 0: if ret.value != OK:
log.info('error has happened in %s', func.__name__) log.info('error has happened in %s', func.__name__)
raise OpusError(ret.value) raise OpusError(ret.value)
return result return result
@ -64,18 +105,53 @@ def _err_ne(result, func, args):
# The third is the result type. # The third is the result type.
# The fourth is the error handler. # The fourth is the error handler.
exported_functions = [ exported_functions = [
# Generic
('opus_get_version_string',
None, ctypes.c_char_p, None),
('opus_strerror', ('opus_strerror',
[ctypes.c_int], ctypes.c_char_p, None), [ctypes.c_int], ctypes.c_char_p, None),
# Encoder functions
('opus_encoder_get_size', ('opus_encoder_get_size',
[ctypes.c_int], ctypes.c_int, None), [ctypes.c_int], ctypes.c_int, None),
('opus_encoder_create', ('opus_encoder_create',
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne), [ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne),
('opus_encode', ('opus_encode',
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt), [EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
('opus_encode_float',
[EncoderStructPtr, c_float_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
('opus_encoder_ctl', ('opus_encoder_ctl',
None, ctypes.c_int32, _err_lt), None, ctypes.c_int32, _err_lt),
('opus_encoder_destroy', ('opus_encoder_destroy',
[EncoderStructPtr], None, None), [EncoderStructPtr], None, None),
# Decoder functions
('opus_decoder_get_size',
[ctypes.c_int], ctypes.c_int, None),
('opus_decoder_create',
[ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
('opus_decode',
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_int16_ptr, ctypes.c_int, ctypes.c_int],
ctypes.c_int, _err_lt),
('opus_decode_float',
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_float_ptr, ctypes.c_int, ctypes.c_int],
ctypes.c_int, _err_lt),
('opus_decoder_ctl',
None, ctypes.c_int32, _err_lt),
('opus_decoder_destroy',
[DecoderStructPtr], None, None),
('opus_decoder_get_nb_samples',
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
# Packet functions
('opus_packet_get_bandwidth',
[ctypes.c_char_p], ctypes.c_int, _err_lt),
('opus_packet_get_nb_channels',
[ctypes.c_char_p], ctypes.c_int, _err_lt),
('opus_packet_get_nb_frames',
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
('opus_packet_get_samples_per_frame',
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
] ]
def libopus_loader(name): def libopus_loader(name):
@ -107,8 +183,9 @@ def _load_default():
try: try:
if sys.platform == 'win32': if sys.platform == 'win32':
_basedir = os.path.dirname(os.path.abspath(__file__)) _basedir = os.path.dirname(os.path.abspath(__file__))
_bitness = 'x64' if sys.maxsize > 2**32 else 'x86' _bitness = struct.calcsize('P') * 8
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_bitness)) _target = 'x64' if _bitness > 32 else 'x86'
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target))
_lib = libopus_loader(_filename) _lib = libopus_loader(_filename)
else: else:
_lib = libopus_loader(ctypes.util.find_library('opus')) _lib = libopus_loader(ctypes.util.find_library('opus'))
@ -188,48 +265,30 @@ class OpusNotLoaded(DiscordException):
"""An exception that is thrown for when libopus is not loaded.""" """An exception that is thrown for when libopus is not loaded."""
pass pass
class _OpusStruct:
# Some constants...
OK = 0
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
APPLICATION_LOWDELAY = 2051
CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
CTL_SET_FEC = 4012
CTL_SET_PLP = 4014
CTL_SET_SIGNAL = 4024
band_ctl = {
'narrow': 1101,
'medium': 1102,
'wide': 1103,
'superwide': 1104,
'full': 1105,
}
signal_ctl = {
'auto': -1000,
'voice': 3001,
'music': 3002,
}
class Encoder:
SAMPLING_RATE = 48000 SAMPLING_RATE = 48000
CHANNELS = 2 CHANNELS = 2
FRAME_LENGTH = 20 FRAME_LENGTH = 20 # in milliseconds
SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16) SAMPLE_SIZE = struct.calcsize('h') * CHANNELS
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH) SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
def __init__(self, application=APPLICATION_AUDIO): @staticmethod
self.application = application def get_opus_version() -> str:
if not is_loaded(): if not is_loaded():
if not _load_default(): if not _load_default():
raise OpusNotLoaded() raise OpusNotLoaded()
return _lib.opus_get_version_string().decode('utf-8')
class Encoder(_OpusStruct):
def __init__(self, application=APPLICATION_AUDIO):
if not is_loaded():
if not _load_default():
raise OpusNotLoaded()
self.application = application
self._state = self._create_state() self._state = self._create_state()
self.set_bitrate(128) self.set_bitrate(128)
self.set_fec(True) self.set_fec(True)
@ -280,3 +339,84 @@ class Encoder:
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes) ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
return array.array('b', data[:ret]).tobytes() return array.array('b', data[:ret]).tobytes()
class Decoder(_OpusStruct):
def __init__(self):
if not is_loaded():
if not _load_default():
raise OpusNotLoaded()
self._state = self._create_state()
def __del__(self):
if hasattr(self, '_state'):
_lib.opus_decoder_destroy(self._state)
self._state = None
def _create_state(self):
ret = ctypes.c_int()
return _lib.opus_decoder_create(self.SAMPLING_RATE, self.CHANNELS, ctypes.byref(ret))
@staticmethod
def packet_get_nb_frames(data):
"""Gets the number of frames in an Opus packet"""
return _lib.opus_packet_get_nb_frames(data, len(data))
@staticmethod
def packet_get_nb_channels(data):
"""Gets the number of channels in an Opus packet"""
return _lib.opus_packet_get_nb_channels(data)
@classmethod
def packet_get_samples_per_frame(cls, data):
"""Gets the number of samples per frame from an Opus packet"""
return _lib.opus_packet_get_samples_per_frame(data, cls.SAMPLING_RATE)
def _set_gain(self, adjustment):
"""Configures decoder gain adjustment.
Scales the decoded output by a factor specified in Q8 dB units.
This has a maximum range of -32768 to 32767 inclusive, and returns
OPUS_BAD_ARG (-1) otherwise. The default is zero indicating no adjustment.
This setting survives decoder reset (irrelevant for now).
gain = 10**x/(20.0*256)
(from opus_defines.h)
"""
return _lib.opus_decoder_ctl(self._state, CTL_SET_GAIN, adjustment)
def set_gain(self, dB):
"""Sets the decoder gain in dB, from -128 to 128."""
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
return self._set_gain(dB_Q8)
def set_volume(self, mult):
"""Sets the output volume as a float percent, i.e. 0.5 for 50%, 1.75 for 175%, etc."""
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
def _get_last_packet_duration(self):
"""Gets the duration (in samples) of the last packet successfully decoded or concealed."""
ret = ctypes.c_int32()
_lib.opus_decoder_ctl(self._state, CTL_LAST_PACKET_DURATION, ctypes.byref(ret))
return ret.value
def decode(self, data, *, fec=False):
if data is None and fec:
raise OpusError("Invalid arguments: FEC cannot be used with null data")
if data is None:
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME
channel_count = self.CHANNELS
else:
frames = self.packet_get_nb_frames(data)
channel_count = self.packet_get_nb_channels(data)
samples_per_frame = self.packet_get_samples_per_frame(data)
frame_size = frames * samples_per_frame
pcm = (ctypes.c_int16 * (frame_size * channel_count))()
pcm_ptr = ctypes.cast(pcm, c_int16_ptr)
ret = _lib.opus_decode(self._state, data, len(data) if data else 0, pcm_ptr, frame_size, fec)
return array.array('h', pcm[:ret * channel_count]).tobytes()

View File

@ -136,9 +136,20 @@ class PartialEmoji(_EmojiTag):
return self.name return self.name
return '%s:%s' % (self.name, self.id) return '%s:%s' % (self.name, self.id)
@property
def created_at(self):
"""Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji.
.. versionadded:: 1.6
"""
if self.is_unicode_emoji():
return None
return utils.snowflake_time(self.id)
@property @property
def url(self): def url(self):
""":class:`Asset`:Returns an asset of the emoji, if it is custom.""" """:class:`Asset`: Returns an asset of the emoji, if it is custom."""
if self.is_unicode_emoji(): if self.is_unicode_emoji():
return Asset(self._state) return Asset(self._state)

View File

@ -382,11 +382,11 @@ class ConnectionState:
return channel or Object(id=channel_id), guild return channel or Object(id=channel_id), guild
async def chunker(self, guild_id, query='', limit=0, *, nonce=None): async def chunker(self, guild_id, query='', limit=0, presences=False, *, nonce=None):
ws = self._get_websocket(guild_id) # This is ignored upstream ws = self._get_websocket(guild_id) # This is ignored upstream
await ws.request_chunks(guild_id, query=query, limit=limit, nonce=nonce) await ws.request_chunks(guild_id, query=query, limit=limit, presences=presences, nonce=nonce)
async def query_members(self, guild, query, limit, user_ids, cache): async def query_members(self, guild, query, limit, user_ids, cache, presences):
guild_id = guild.id guild_id = guild.id
ws = self._get_websocket(guild_id) ws = self._get_websocket(guild_id)
if ws is None: if ws is None:
@ -397,7 +397,7 @@ class ConnectionState:
try: try:
# start the query operation # start the query operation
await ws.request_chunks(guild_id, query=query, limit=limit, user_ids=user_ids, nonce=request.nonce) await ws.request_chunks(guild_id, query=query, limit=limit, user_ids=user_ids, presences=presences, nonce=request.nonce)
return await asyncio.wait_for(request.wait(), timeout=30.0) return await asyncio.wait_for(request.wait(), timeout=30.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
log.warning('Timed out waiting for chunks with query %r and limit %d for guild_id %d', query, limit, guild_id) log.warning('Timed out waiting for chunks with query %r and limit %d for guild_id %d', query, limit, guild_id)
@ -688,8 +688,6 @@ class ConnectionState:
log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type']) log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type'])
return return
channel = None
if ch_type in (ChannelType.group, ChannelType.private): if ch_type in (ChannelType.group, ChannelType.private):
channel_id = int(data['id']) channel_id = int(data['id'])
if self._get_private_channel(channel_id) is None: if self._get_private_channel(channel_id) is None:
@ -795,6 +793,12 @@ class ConnectionState:
else: else:
if self.member_cache_flags.joined: if self.member_cache_flags.joined:
member = Member(data=data, guild=guild, state=self) member = Member(data=data, guild=guild, state=self)
# Force an update on the inner user if necessary
user_update = member._update_inner_user(user)
if user_update:
self.dispatch('user_update', user_update[0], user_update[1])
guild._add_member(member) guild._add_member(member)
log.debug('GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.', user_id) log.debug('GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.', user_id)
@ -969,8 +973,19 @@ class ConnectionState:
def parse_guild_members_chunk(self, data): def parse_guild_members_chunk(self, data):
guild_id = int(data['guild_id']) guild_id = int(data['guild_id'])
guild = self._get_guild(guild_id) guild = self._get_guild(guild_id)
presences = data.get('presences', [])
members = [Member(guild=guild, data=member, state=self) for member in data.get('members', [])] members = [Member(guild=guild, data=member, state=self) for member in data.get('members', [])]
log.debug('Processed a chunk for %s members in guild ID %s.', len(members), guild_id) log.debug('Processed a chunk for %s members in guild ID %s.', len(members), guild_id)
if presences:
member_dict = {str(member.id): member for member in members}
for presence in presences:
user = presence['user']
member_id = user['id']
member = member_dict.get(member_id)
member._presence_update(presence, user)
complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count')
self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) self.process_chunk_requests(guild_id, data.get('nonce'), members, complete)
@ -1123,9 +1138,9 @@ class AutoShardedConnectionState(ConnectionState):
channel = new_guild.get_channel(channel_id) or Object(id=channel_id) channel = new_guild.get_channel(channel_id) or Object(id=channel_id)
msg._rebind_channel_reference(channel) msg._rebind_channel_reference(channel)
async def chunker(self, guild_id, query='', limit=0, *, shard_id=None, nonce=None): async def chunker(self, guild_id, query='', limit=0, presences=False, *, shard_id=None, nonce=None):
ws = self._get_websocket(guild_id, shard_id=shard_id) ws = self._get_websocket(guild_id, shard_id=shard_id)
await ws.request_chunks(guild_id, query=query, limit=limit, nonce=nonce) await ws.request_chunks(guild_id, query=query, limit=limit, presences=presences, nonce=nonce)
async def _delay_ready(self): async def _delay_ready(self):
await self.shards_launched.wait() await self.shards_launched.wait()

View File

@ -35,6 +35,7 @@ import aiohttp
from . import utils from . import utils
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound, DiscordServerError from .errors import InvalidArgument, HTTPException, Forbidden, NotFound, DiscordServerError
from .message import Message
from .enums import try_enum, WebhookType from .enums import try_enum, WebhookType
from .user import BaseUser, User from .user import BaseUser, User
from .asset import Asset from .asset import Asset
@ -45,6 +46,7 @@ __all__ = (
'AsyncWebhookAdapter', 'AsyncWebhookAdapter',
'RequestsWebhookAdapter', 'RequestsWebhookAdapter',
'Webhook', 'Webhook',
'WebhookMessage',
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -66,6 +68,9 @@ class WebhookAdapter:
self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token) self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token)
self.webhook = webhook self.webhook = webhook
def is_async(self):
return False
def request(self, verb, url, payload=None, multipart=None): def request(self, verb, url, payload=None, multipart=None):
"""Actually does the request. """Actually does the request.
@ -94,6 +99,12 @@ class WebhookAdapter:
def edit_webhook(self, *, reason=None, **payload): def edit_webhook(self, *, reason=None, **payload):
return self.request('PATCH', self._request_url, payload=payload, reason=reason) return self.request('PATCH', self._request_url, payload=payload, reason=reason)
def edit_webhook_message(self, message_id, payload):
return self.request('PATCH', '{}/messages/{}'.format(self._request_url, message_id), payload=payload)
def delete_webhook_message(self, message_id):
return self.request('DELETE', '{}/messages/{}'.format(self._request_url, message_id))
def handle_execution_response(self, data, *, wait): def handle_execution_response(self, data, *, wait):
"""Transforms the webhook execution response into something """Transforms the webhook execution response into something
more meaningful. more meaningful.
@ -178,6 +189,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
self.session = session self.session = session
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
def is_async(self):
return True
async def request(self, verb, url, payload=None, multipart=None, *, files=None, reason=None): async def request(self, verb, url, payload=None, multipart=None, *, files=None, reason=None):
headers = {} headers = {}
data = None data = None
@ -253,8 +267,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
return data return data
# transform into Message object # transform into Message object
from .message import Message # Make sure to coerce the state to the partial one to allow message edits/delete
return Message(data=data, state=self.webhook._state, channel=self.webhook.channel) state = _PartialWebhookState(self, self.webhook, parent=self.webhook._state)
return WebhookMessage(data=data, state=state, channel=self.webhook.channel)
class RequestsWebhookAdapter(WebhookAdapter): class RequestsWebhookAdapter(WebhookAdapter):
"""A webhook adapter suited for use with ``requests``. """A webhook adapter suited for use with ``requests``.
@ -356,8 +371,9 @@ class RequestsWebhookAdapter(WebhookAdapter):
return response return response
# transform into Message object # transform into Message object
from .message import Message # Make sure to coerce the state to the partial one to allow message edits/delete
return Message(data=response, state=self.webhook._state, channel=self.webhook.channel) state = _PartialWebhookState(self, self.webhook, parent=self.webhook._state)
return WebhookMessage(data=response, state=state, channel=self.webhook.channel)
class _FriendlyHttpAttributeErrorHelper: class _FriendlyHttpAttributeErrorHelper:
__slots__ = () __slots__ = ()
@ -366,9 +382,16 @@ class _FriendlyHttpAttributeErrorHelper:
raise AttributeError('PartialWebhookState does not support http methods.') raise AttributeError('PartialWebhookState does not support http methods.')
class _PartialWebhookState: class _PartialWebhookState:
__slots__ = ('loop',) __slots__ = ('loop', 'parent', '_webhook')
def __init__(self, adapter, webhook, parent):
self._webhook = webhook
if isinstance(parent, self.__class__):
self.parent = None
else:
self.parent = parent
def __init__(self, adapter):
# Fetch the loop from the adapter if it's there # Fetch the loop from the adapter if it's there
try: try:
self.loop = adapter.loop self.loop = adapter.loop
@ -387,13 +410,111 @@ class _PartialWebhookState:
@property @property
def http(self): def http(self):
if self.parent is not None:
return self.parent.http
# Some data classes assign state.http and that should be kosher # Some data classes assign state.http and that should be kosher
# however, using it should result in a late-binding error. # however, using it should result in a late-binding error.
return _FriendlyHttpAttributeErrorHelper() return _FriendlyHttpAttributeErrorHelper()
def __getattr__(self, attr): def __getattr__(self, attr):
if self.parent is not None:
return getattr(self.parent, attr)
raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr)) raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr))
class WebhookMessage(Message):
"""Represents a message sent from your webhook.
This allows you to edit or delete a message sent by your
webhook.
This inherits from :class:`discord.Message` with changes to
:meth:`edit` and :meth:`delete` to work.
.. versionadded:: 1.6
"""
def edit(self, **fields):
"""|maybecoro|
Edits the message.
The content must be able to be transformed into a string via ``str(content)``.
.. versionadded:: 1.6
Parameters
------------
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
InvalidArgument
You specified both ``embed`` and ``embeds`` or the length of
``embeds`` was invalid or there was no token associated with
this webhook.
"""
return self._state._webhook.edit_message(self.id, **fields)
def _delete_delay_sync(self, delay):
time.sleep(delay)
return self._state._webhook.delete_message(self.id)
async def _delete_delay_async(self, delay):
async def inner_call():
await asyncio.sleep(delay)
try:
await self._state._webhook.delete_message(self.id)
except HTTPException:
pass
asyncio.ensure_future(inner_call(), loop=self._state.loop)
return await asyncio.sleep(0)
def delete(self, *, delay=None):
"""|coro|
Deletes the message.
Parameters
-----------
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
If this is a coroutine, the waiting is done in the background and deletion failures
are ignored. If this is not a coroutine then the delay blocks the thread.
Raises
------
Forbidden
You do not have proper permissions to delete the message.
NotFound
The message was deleted already.
HTTPException
Deleting the message failed.
"""
if delay is not None:
if self._state.parent._adapter.is_async():
return self._delete_delay_async(delay)
else:
return self._delete_delay_sync(delay)
return self._state._webhook.delete_message(self.id)
class Webhook(Hashable): class Webhook(Hashable):
"""Represents a Discord webhook. """Represents a Discord webhook.
@ -488,7 +609,7 @@ class Webhook(Hashable):
self.name = data.get('name') self.name = data.get('name')
self.avatar = data.get('avatar') self.avatar = data.get('avatar')
self.token = data.get('token') self.token = data.get('token')
self._state = state or _PartialWebhookState(adapter) self._state = state or _PartialWebhookState(adapter, self, parent=state)
self._adapter = adapter self._adapter = adapter
self._adapter._prepare(self) self._adapter._prepare(self)
@ -785,7 +906,7 @@ class Webhook(Hashable):
wait: :class:`bool` wait: :class:`bool`
Whether the server should wait before sending a response. This essentially Whether the server should wait before sending a response. This essentially
means that the return type of this function changes from ``None`` to means that the return type of this function changes from ``None`` to
a :class:`Message` if set to ``True``. a :class:`WebhookMessage` if set to ``True``.
username: :class:`str` username: :class:`str`
The username to send with this message. If no username is provided The username to send with this message. If no username is provided
then the default username for the webhook is used. then the default username for the webhook is used.
@ -825,7 +946,7 @@ class Webhook(Hashable):
Returns Returns
--------- ---------
Optional[:class:`Message`] Optional[:class:`WebhookMessage`]
The message that was sent. The message that was sent.
""" """
@ -869,3 +990,115 @@ class Webhook(Hashable):
def execute(self, *args, **kwargs): def execute(self, *args, **kwargs):
"""An alias for :meth:`~.Webhook.send`.""" """An alias for :meth:`~.Webhook.send`."""
return self.send(*args, **kwargs) return self.send(*args, **kwargs)
def edit_message(self, message_id, **fields):
"""|maybecoro|
Edits a message owned by this webhook.
This is a lower level interface to :meth:`WebhookMessage.edit` in case
you only have an ID.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to edit.
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
InvalidArgument
You specified both ``embed`` and ``embeds`` or the length of
``embeds`` was invalid or there was no token associated with
this webhook.
"""
payload = {}
if self.token is None:
raise InvalidArgument('This webhook does not have a token associated with it')
try:
content = fields['content']
except KeyError:
pass
else:
if content is not None:
content = str(content)
payload['content'] = content
# Check if the embeds interface is being used
try:
embeds = fields['embeds']
except KeyError:
# Nope
pass
else:
if embeds is None or len(embeds) > 10:
raise InvalidArgument('embeds has a maximum of 10 elements')
payload['embeds'] = [e.to_dict() for e in embeds]
try:
embed = fields['embed']
except KeyError:
pass
else:
if 'embeds' in payload:
raise InvalidArgument('Cannot mix embed and embeds keyword arguments')
if embed is None:
payload['embeds'] = []
else:
payload['embeds'] = [embed.to_dict()]
allowed_mentions = fields.pop('allowed_mentions', None)
previous_mentions = getattr(self._state, 'allowed_mentions', None)
if allowed_mentions:
if previous_mentions is not None:
payload['allowed_mentions'] = previous_mentions.merge(allowed_mentions).to_dict()
else:
payload['allowed_mentions'] = allowed_mentions.to_dict()
elif previous_mentions is not None:
payload['allowed_mentions'] = previous_mentions.to_dict()
return self._adapter.edit_webhook_message(message_id, payload=payload)
def delete_message(self, message_id):
"""|maybecoro|
Deletes a message owned by this webhook.
This is a lower level interface to :meth:`WebhookMessage.delete` in case
you only have an ID.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to delete.
Raises
-------
HTTPException
Deleting the message failed.
Forbidden
Deleted a message that is not yours.
"""
return self._adapter.delete_webhook_message(message_id)

View File

@ -112,6 +112,7 @@
:root[data-theme="dark"] .highlight .ni { color: #d0d0d0; } /* Name.Entity */ :root[data-theme="dark"] .highlight .ni { color: #d0d0d0; } /* Name.Entity */
:root[data-theme="dark"] .highlight .ne { color: #bbbbbb; } /* Name.Exception */ :root[data-theme="dark"] .highlight .ne { color: #bbbbbb; } /* Name.Exception */
:root[data-theme="dark"] .highlight .nf { color: #6494d8; } /* Name.Function */ :root[data-theme="dark"] .highlight .nf { color: #6494d8; } /* Name.Function */
:root[data-theme="dark"] .highlight .fm { color: #6494d8; } /* Name.Function.Magic */
:root[data-theme="dark"] .highlight .nl { color: #d0d0d0; } /* Name.Label */ :root[data-theme="dark"] .highlight .nl { color: #d0d0d0; } /* Name.Label */
:root[data-theme="dark"] .highlight .nn { color: #6494d8;} /* Name.Namespace */ :root[data-theme="dark"] .highlight .nn { color: #6494d8;} /* Name.Namespace */
:root[data-theme="dark"] .highlight .nx { color: #d0d0d0; } /* Name.Other */ :root[data-theme="dark"] .highlight .nx { color: #d0d0d0; } /* Name.Other */

View File

@ -5,6 +5,7 @@ let bottomHeightThreshold, sections;
let hamburgerToggle; let hamburgerToggle;
let mobileSearch; let mobileSearch;
let sidebar; let sidebar;
let toTop;
class Modal { class Modal {
constructor(element) { constructor(element) {
@ -49,12 +50,19 @@ class SearchBar {
} }
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
mobileSearch = new SearchBar(); mobileSearch = new SearchBar();
bottomHeightThreshold = document.documentElement.scrollHeight - 30; bottomHeightThreshold = document.documentElement.scrollHeight - 30;
sections = document.querySelectorAll('section'); sections = document.querySelectorAll('section');
hamburgerToggle = document.getElementById('hamburger-toggle'); hamburgerToggle = document.getElementById('hamburger-toggle');
toTop = document.getElementById('to-top');
toTop.hidden = !(window.scrollY > 0);
if (hamburgerToggle) { if (hamburgerToggle) {
hamburgerToggle.addEventListener('click', (e) => { hamburgerToggle.addEventListener('click', (e) => {
@ -76,6 +84,16 @@ document.addEventListener('DOMContentLoaded', () => {
// insert ourselves after the element // insert ourselves after the element
parent.insertBefore(table, element.nextSibling); parent.insertBefore(table, element.nextSibling);
}); });
window.addEventListener('scroll', () => {
toTop.hidden = !(window.scrollY > 0);
});
});
document.addEventListener('keydown', (event) => {
if (event.code == "Escape" && activeModal) {
activeModal.close();
}
}); });
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {

View File

@ -94,10 +94,13 @@ function updateSetting(element) {
} }
} }
for (const setting of settings) {
setting.load();
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
settingsModal = new Modal(document.querySelector('div#settings.modal')); settingsModal = new Modal(document.querySelector('div#settings.modal'));
for (const setting of settings) { for (const setting of settings) {
setting.load();
setting.setElement(); setting.setElement();
} }
}); });

View File

@ -19,6 +19,7 @@ Historically however, thanks to:
/* CSS variables would go here */ /* CSS variables would go here */
:root { :root {
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--monospace-font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
/* palette goes here */ /* palette goes here */
--white: #ffffff; --white: #ffffff;
@ -96,6 +97,8 @@ Historically however, thanks to:
--rtd-ad-background: var(--grey-2); --rtd-ad-background: var(--grey-2);
--rtd-ad-main-text: var(--grey-6); --rtd-ad-main-text: var(--grey-6);
--rtd-ad-small-text: var(--grey-4); --rtd-ad-small-text: var(--grey-4);
--rtd-version-background: #272525;
--rtd-version-main-text: #fcfcfc;
--attribute-table-title: var(--grey-6); --attribute-table-title: var(--grey-6);
--attribute-table-entry-border: var(--grey-3); --attribute-table-entry-border: var(--grey-3);
--attribute-table-entry-text: var(--grey-5); --attribute-table-entry-text: var(--grey-5);
@ -103,6 +106,7 @@ Historically however, thanks to:
--attribute-table-entry-hover-background: var(--grey-2); --attribute-table-entry-hover-background: var(--grey-2);
--attribute-table-entry-hover-text: var(--blue-2); --attribute-table-entry-hover-text: var(--blue-2);
--attribute-table-badge: var(--grey-7); --attribute-table-badge: var(--grey-7);
--highlighted-text: rgb(252, 233, 103);
} }
:root[data-font="serif"] { :root[data-font="serif"] {
@ -162,6 +166,7 @@ Historically however, thanks to:
--attribute-table-entry-hover-background: var(--grey-6); --attribute-table-entry-hover-background: var(--grey-6);
--attribute-table-entry-hover-text: var(--blue-1); --attribute-table-entry-hover-text: var(--blue-1);
--attribute-table-badge: var(--grey-4); --attribute-table-badge: var(--grey-4);
--highlighted-text: rgba(250, 166, 26, 0.2);
} }
img[src$="snake_dark.svg"] { img[src$="snake_dark.svg"] {
@ -247,6 +252,7 @@ header > nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center;
} }
header > nav a { header > nav a {
@ -265,6 +271,12 @@ header > nav.mobile-only {
header > nav.mobile-only .search { header > nav.mobile-only .search {
width: 100%; width: 100%;
position: absolute;
top: 0;
right: 0;
z-index: -1;
padding-top: 0;
transition: top 0.5s ease-in-out;
} }
header > nav.mobile-only .search-wrapper { header > nav.mobile-only .search-wrapper {
@ -316,6 +328,11 @@ header > nav > a:hover {
cursor: pointer; cursor: pointer;
} }
.sub-header option {
color: black;
}
.sub-header > select:focus { .sub-header > select:focus {
outline: none; outline: none;
} }
@ -380,12 +397,12 @@ aside h3 {
position: relative; position: relative;
line-height: 0.5em; line-height: 0.5em;
transition: transform 0.4s; transition: transform 0.4s;
transform: rotate(0deg); transform: rotate(-90deg);
} }
.expanded { .expanded {
transition: transform 0.4s; transition: transform 0.4s;
transform: rotate(-90deg); transform: rotate(0deg);
} }
.ref-internal-padding { .ref-internal-padding {
@ -567,6 +584,37 @@ div.modal input {
cursor: pointer; cursor: pointer;
} }
/* scroll to top button */
#to-top {
position: fixed;
bottom: 50px;
right: 20px;
cursor: pointer;
}
#to-top.is-rtd {
bottom: 90px;
}
#to-top > span {
display: block;
width: auto;
height: 30px;
padding: 0 6px;
background-color: var(--rtd-version-background);
color: var(--rtd-version-main-text);
}
#to-top span {
line-height: 30px;
font-size: 90%;
text-align: center;
}
/* copy button */ /* copy button */
.relative-copy { .relative-copy {
@ -855,7 +903,7 @@ dl.field-list {
/* internal references are forced to bold for some reason */ /* internal references are forced to bold for some reason */
a.reference.internal > strong { a.reference.internal > strong {
font-weight: unset; font-weight: unset;
font-family: monospace; font-family: var(--monospace-font-family);
} }
/* exception hierarchy */ /* exception hierarchy */
@ -950,7 +998,7 @@ pre {
} }
pre, code { pre, code {
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-family: var(--monospace-font-family);
font-size: 0.9em; font-size: 0.9em;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@ -1007,6 +1055,13 @@ dd {
margin-left: 1.5em; margin-left: 1.5em;
} }
dt:target, span.highlighted {
background-color: var(--highlighted-text);
}
rect.highlighted {
fill: var(--highlighted-text);
}
.container.operations { .container.operations {
padding: 10px; padding: 10px;

View File

@ -67,7 +67,7 @@
<a onclick="mobileSearch.close();" title="{{ _('Close') }}" id="close-search" class="mobile-only" hidden><span class="material-icons">close</span></a> <a onclick="mobileSearch.close();" title="{{ _('Close') }}" id="close-search" class="mobile-only" hidden><span class="material-icons">close</span></a>
</nav> </nav>
<nav class="mobile-only"> <nav class="mobile-only">
<form role="search" class="search" action="search.html" method="get"> <form role="search" class="search" action="{{ pathto('search') }}" method="get">
<div class="search-wrapper"> <div class="search-wrapper">
<input type="search" name="q" placeholder="{{ _('Search documentation') }}" /> <input type="search" name="q" placeholder="{{ _('Search documentation') }}" />
<button type="submit"> <button type="submit">
@ -90,7 +90,7 @@
<option value="{{ pathto(p + '/index')|e }}" {% if pagename is prefixedwith p %}selected{% endif %}>{{ ext }}</option> <option value="{{ pathto(p + '/index')|e }}" {% if pagename is prefixedwith p %}selected{% endif %}>{{ ext }}</option>
{%- endfor %} {%- endfor %}
</select> </select>
<form role="search" class="search" action="search.html" method="get"> <form role="search" class="search" action="{{ pathto('search') }}" method="get">
<div class="search-wrapper"> <div class="search-wrapper">
<input type="search" name="q" placeholder="{{ _('Search documentation') }}" /> <input type="search" name="q" placeholder="{{ _('Search documentation') }}" />
<button type="submit"> <button type="submit">
@ -115,7 +115,7 @@
</div> </div>
</aside> </aside>
{#- The actual body of the contents #} {#- The actual body of the contents #}
<main class="grid-item"> <main class="grid-item" role="main">
{% block body %} {% endblock %} {% block body %} {% endblock %}
</main> </main>
{%- block footer %} {%- block footer %}
@ -190,5 +190,9 @@
</div> </div>
</div> </div>
<div id="to-top" onclick="scrollToTop()"{%- if READTHEDOCS %} class="is-rtd"{%- endif %} hidden>
<span><span class="material-icons">arrow_upward</span> to top</span>
</div>
</body> </body>
</html> </html>

View File

@ -87,6 +87,14 @@ VoiceClient
.. autoclass:: VoiceClient() .. autoclass:: VoiceClient()
:members: :members:
VoiceProtocol
~~~~~~~~~~~~~~~
.. attributetable:: VoiceProtocol
.. autoclass:: VoiceProtocol
:members:
AudioSource AudioSource
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -147,7 +155,7 @@ Opus Library
Event Reference Event Reference
--------------- ---------------
This page outlines the different types of events listened by :class:`Client`. This section outlines the different types of events listened by :class:`Client`.
There are two ways to register an event, the first way is through the use of There are two ways to register an event, the first way is through the use of
:meth:`Client.event`. The second way is through subclassing :class:`Client` and :meth:`Client.event`. The second way is through subclassing :class:`Client` and
@ -661,6 +669,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
- activity - activity
- nickname - nickname
- roles - roles
- pending
This requires :attr:`Intents.members` to be enabled. This requires :attr:`Intents.members` to be enabled.
@ -2150,9 +2159,9 @@ Certain utilities make working with async iterators easier, detailed below.
Collects items into chunks of up to a given maximum size. Collects items into chunks of up to a given maximum size.
Another :class:`AsyncIterator` is returned which collects items into Another :class:`AsyncIterator` is returned which collects items into
:class:`list`\s of a given size. The maximum chunk size must be a positive integer. :class:`list`\s of a given size. The maximum chunk size must be a positive integer.
.. versionadded:: 1.6 .. versionadded:: 1.6
Collecting groups of users: :: Collecting groups of users: ::
async for leader, *users in reaction.users().chunk(3): async for leader, *users in reaction.users().chunk(3):
@ -2616,11 +2625,22 @@ Webhook Support
discord.py offers support for creating, editing, and executing webhooks through the :class:`Webhook` class. discord.py offers support for creating, editing, and executing webhooks through the :class:`Webhook` class.
Webhook
~~~~~~~~~
.. attributetable:: Webhook .. attributetable:: Webhook
.. autoclass:: Webhook .. autoclass:: Webhook
:members: :members:
WebhookMessage
~~~~~~~~~~~~~~~~
.. attributetable:: WebhookMessage
.. autoclass:: WebhookMessage
:members:
Adapters Adapters
~~~~~~~~~ ~~~~~~~~~
@ -2790,6 +2810,8 @@ Message
DeletedReferencedMessage DeletedReferencedMessage
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: DeletedReferencedMessage
.. autoclass:: DeletedReferencedMessage() .. autoclass:: DeletedReferencedMessage()
:members: :members:
@ -2998,6 +3020,8 @@ Invite
Template Template
~~~~~~~~~ ~~~~~~~~~
.. attributetable:: Template
.. autoclass:: Template() .. autoclass:: Template()
:members: :members:
@ -3029,6 +3053,8 @@ Widget
Sticker Sticker
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
.. attributetable:: Sticker
.. autoclass:: Sticker() .. autoclass:: Sticker()
:members: :members:
@ -3101,6 +3127,8 @@ dynamic attributes in mind.
Object Object
~~~~~~~ ~~~~~~~
.. attributetable:: Object
.. autoclass:: Object .. autoclass:: Object
:members: :members:
@ -3126,6 +3154,12 @@ MessageReference
.. autoclass:: MessageReference .. autoclass:: MessageReference
:members: :members:
PartialMessage
~~~~~~~~~~~~~~~~~
.. autoclass:: PartialMessage
:members:
Intents Intents
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -40,6 +40,7 @@ extensions = [
'details', 'details',
'exception_hierarchy', 'exception_hierarchy',
'attributetable', 'attributetable',
'resourcelinks',
] ]
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
@ -76,7 +77,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'discord.py' project = u'discord.py'
copyright = u'2015-2020, Rapptz' copyright = u'2015-2021, Rapptz'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -91,6 +92,9 @@ with open('../discord/__init__.py') as f:
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
# This assumes a tag is available for final releases
branch = 'master' if version.endswith('a') else 'v' + version
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# #
@ -152,6 +156,13 @@ html_context = {
], ],
} }
resource_links = {
'discord': 'https://discord.gg/r3sSKJJ',
'issues': 'https://github.com/Rapptz/discord.py/issues',
'discussions': 'https://github.com/Rapptz/discord.py/discussions',
'examples': 'https://github.com/Rapptz/discord.py/tree/%s/examples' % branch,
}
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
@ -337,3 +348,4 @@ def setup(app):
if app.config.language == 'ja': if app.config.language == 'ja':
app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None) app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None)
app.config.html_context['discord_invite'] = 'https://discord.gg/nXzj3dg' app.config.html_context['discord_invite'] = 'https://discord.gg/nXzj3dg'
app.config.resource_links['discord'] = 'https://discord.gg/nXzj3dg'

View File

@ -35,7 +35,7 @@ Creating a Bot account is a pretty straightforward process.
7. Copy the token using the "Copy" button. 7. Copy the token using the "Copy" button.
- **This is not the Client Secret at the General Information page** - **This is not the Client Secret at the General Information page.**
.. warning:: .. warning::

View File

@ -7,16 +7,29 @@ The following section outlines the API of discord.py's command extension module.
.. _ext_commands_api_bot: .. _ext_commands_api_bot:
Bots
------
Bot Bot
---- ~~~~
.. attributetable:: discord.ext.commands.Bot
.. autoclass:: discord.ext.commands.Bot .. autoclass:: discord.ext.commands.Bot
:members: :members:
:inherited-members: :inherited-members:
AutoShardedBot
~~~~~~~~~~~~~~~~
.. attributetable:: discord.ext.commands.AutoShardedBot
.. autoclass:: discord.ext.commands.AutoShardedBot .. autoclass:: discord.ext.commands.AutoShardedBot
:members: :members:
Prefix Helpers
----------------
.. autofunction:: discord.ext.commands.when_mentioned .. autofunction:: discord.ext.commands.when_mentioned
.. autofunction:: discord.ext.commands.when_mentioned_or .. autofunction:: discord.ext.commands.when_mentioned_or
@ -64,21 +77,39 @@ are custom to the command extension module.
.. _ext_commands_api_command: .. _ext_commands_api_command:
Command Commands
-------- ----------
Decorators
~~~~~~~~~~~~
.. autofunction:: discord.ext.commands.command .. autofunction:: discord.ext.commands.command
.. autofunction:: discord.ext.commands.group .. autofunction:: discord.ext.commands.group
Command
~~~~~~~~~
.. attributetable:: discord.ext.commands.Command
.. autoclass:: discord.ext.commands.Command .. autoclass:: discord.ext.commands.Command
:members: :members:
:special-members: __call__ :special-members: __call__
Group
~~~~~~
.. attributetable:: discord.ext.commands.Group
.. autoclass:: discord.ext.commands.Group .. autoclass:: discord.ext.commands.Group
:members: :members:
:inherited-members: :inherited-members:
GroupMixin
~~~~~~~~~~~
.. attributetable:: discord.ext.commands.GroupMixin
.. autoclass:: discord.ext.commands.GroupMixin .. autoclass:: discord.ext.commands.GroupMixin
:members: :members:
@ -87,28 +118,58 @@ Command
Cogs Cogs
------ ------
Cog
~~~~
.. attributetable:: discord.ext.commands.Cog
.. autoclass:: discord.ext.commands.Cog .. autoclass:: discord.ext.commands.Cog
:members: :members:
CogMeta
~~~~~~~~
.. attributetable:: discord.ext.commands.CogMeta
.. autoclass:: discord.ext.commands.CogMeta .. autoclass:: discord.ext.commands.CogMeta
:members: :members:
.. _ext_commands_help_command: .. _ext_commands_help_command:
Help Commands Help Commands
----------------- ---------------
HelpCommand
~~~~~~~~~~~~
.. attributetable:: discord.ext.commands.HelpCommand
.. autoclass:: discord.ext.commands.HelpCommand .. autoclass:: discord.ext.commands.HelpCommand
:members: :members:
DefaultHelpCommand
~~~~~~~~~~~~~~~~~~~
.. attributetable:: discord.ext.commands.DefaultHelpCommand
.. autoclass:: discord.ext.commands.DefaultHelpCommand .. autoclass:: discord.ext.commands.DefaultHelpCommand
:members: :members:
:exclude-members: send_bot_help, send_cog_help, send_group_help, send_command_help, prepare_help_command :exclude-members: send_bot_help, send_cog_help, send_group_help, send_command_help, prepare_help_command
MinimalHelpCommand
~~~~~~~~~~~~~~~~~~~
.. attributetable:: discord.ext.commands.MinimalHelpCommand
.. autoclass:: discord.ext.commands.MinimalHelpCommand .. autoclass:: discord.ext.commands.MinimalHelpCommand
:members: :members:
:exclude-members: send_bot_help, send_cog_help, send_group_help, send_command_help, prepare_help_command :exclude-members: send_bot_help, send_cog_help, send_group_help, send_command_help, prepare_help_command
Paginator
~~~~~~~~~~
.. attributetable:: discord.ext.commands.Paginator
.. autoclass:: discord.ext.commands.Paginator .. autoclass:: discord.ext.commands.Paginator
:members: :members:
@ -190,6 +251,8 @@ Checks
Context Context
-------- --------
.. attributetable:: discord.ext.commands.Context
.. autoclass:: discord.ext.commands.Context .. autoclass:: discord.ext.commands.Context
:members: :members:
:inherited-members: :inherited-members:
@ -353,9 +416,15 @@ Exceptions
.. autoexception:: discord.ext.commands.ChannelNotReadable .. autoexception:: discord.ext.commands.ChannelNotReadable
:members: :members:
.. autoexception:: discord.ext.commands.BadColourArgument
:members:
.. autoexception:: discord.ext.commands.RoleNotFound .. autoexception:: discord.ext.commands.RoleNotFound
:members: :members:
.. autoexception:: discord.ext.commands.BadInviteArgument
:members:
.. autoexception:: discord.ext.commands.EmojiNotFound .. autoexception:: discord.ext.commands.EmojiNotFound
:members: :members:
@ -409,7 +478,7 @@ Exceptions
Exception Hierarchy Exception Hierarchy
+++++++++++++++++++++ ~~~~~~~~~~~~~~~~~~~~~
.. exception_hierarchy:: .. exception_hierarchy::

View File

@ -713,7 +713,7 @@ Global Checks
Sometimes we want to apply a check to **every** command, not just certain commands. The library supports this as well Sometimes we want to apply a check to **every** command, not just certain commands. The library supports this as well
using the global check concept. using the global check concept.
Global checks work similarly to regular checks except they are registered with the :func:`.Bot.check` decorator. Global checks work similarly to regular checks except they are registered with the :meth:`.Bot.check` decorator.
For example, to block all DMs we could do the following: For example, to block all DMs we could do the following:

View File

@ -135,6 +135,8 @@ Doing something during cancellation:
API Reference API Reference
--------------- ---------------
.. attributetable:: discord.ext.tasks.Loop
.. autoclass:: discord.ext.tasks.Loop() .. autoclass:: discord.ext.tasks.Loop()
:members: :members:

View File

@ -0,0 +1,44 @@
# Credit to sphinx.ext.extlinks for being a good starter
# Copyright 2007-2020 by the Sphinx team
# Licensed under BSD.
from typing import Any, Dict, List, Tuple
from docutils import nodes, utils
from docutils.nodes import Node, system_message
from docutils.parsers.rst.states import Inliner
import sphinx
from sphinx.application import Sphinx
from sphinx.util.nodes import split_explicit_title
from sphinx.util.typing import RoleFunction
def make_link_role(resource_links: Dict[str, str]) -> RoleFunction:
def role(
typ: str,
rawtext: str,
text: str,
lineno: int,
inliner: Inliner,
options: Dict = {},
content: List[str] = []
) -> Tuple[List[Node], List[system_message]]:
text = utils.unescape(text)
has_explicit_title, title, key = split_explicit_title(text)
full_url = resource_links[key]
if not has_explicit_title:
title = full_url
pnode = nodes.reference(title, title, internal=False, refuri=full_url)
return [pnode], []
return role
def add_link_role(app: Sphinx) -> None:
app.add_role('resource', make_link_role(app.config.resource_links))
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('resource_links', {}, 'env')
app.connect('builder-inited', add_link_role)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -30,41 +30,57 @@ Features
- Easy to use with an object oriented design - Easy to use with an object oriented design
- Optimised for both speed and memory - Optimised for both speed and memory
Documentation Contents Getting started
----------------------- -----------------
.. toctree:: Is this your first time using the library? This is the place to get started!
:maxdepth: 2
intro - **First steps:** :doc:`intro` | :doc:`quickstart` | :doc:`logging`
quickstart - **Working with Discord:** :doc:`discord` | :doc:`intents`
migrating - **Examples:** Many examples are available in the :resource:`repository <examples>`.
logging
api Getting help
--------------
If you're having trouble with something, these resources might help.
- Try the :doc:`faq` first, it's got answers to all common questions.
- Ask us and hang out with us in our :resource:`Discord <discord>` server.
- If you're looking for something specific, try the :ref:`index <genindex>` or :ref:`searching <search>`.
- Report bugs in the :resource:`issue tracker <issues>`.
- Ask in our :resource:`GitHub discussions page <discussions>`.
Extensions Extensions
----------- ------------
These extensions help you during development when it comes to common tasks.
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 1
ext/commands/index.rst ext/commands/index.rst
ext/tasks/index.rst ext/tasks/index.rst
Manuals
---------
Additional Information These pages go into great detail about everything the API can do.
-----------------------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 1
discord api
intents discord.ext.commands API Reference <ext/commands/api.rst>
faq discord.ext.tasks API Reference <ext/tasks/index.rst>
whats_new
version_guarantees
If you still can't find what you're looking for, try in one of the following pages: Meta
------
* :ref:`genindex` If you're looking for something related to the project itself, it's here.
* :ref:`search`
.. toctree::
:maxdepth: 1
whats_new
version_guarantees
migrating

View File

@ -60,7 +60,7 @@ With the API change requiring bot authors to specify intents, some intents were
A privileged intent is one that requires you to go to the developer portal and manually enable it. To enable privileged intents do the following: A privileged intent is one that requires you to go to the developer portal and manually enable it. To enable privileged intents do the following:
1. Make sure you're logged on to the `Discord website <https://discord.com>`_. 1. Make sure you're logged on to the `Discord website <https://discord.com>`_.
2. Navigate to the `application page <https://discord.com/developers/applications>`_ 2. Navigate to the `application page <https://discord.com/developers/applications>`_.
3. Click on the bot you want to enable privileged intents for. 3. Click on the bot you want to enable privileged intents for.
4. Navigate to the bot tab on the left side of the screen. 4. Navigate to the bot tab on the left side of the screen.
@ -74,7 +74,7 @@ A privileged intent is one that requires you to go to the developer portal and m
.. warning:: .. warning::
Enabling privileged intents when your bot is in over 100 guilds requires going through `bot verification <https://support.discord.com/hc/en-us/articles/360040720412>`_. If your bot is already verified and you would like to enable a privileged intent you must go through `discord support <https://dis.gd/contact>`_ and talk to them about it. Enabling privileged intents when your bot is in over 100 guilds requires going through `bot verification <https://support.discord.com/hc/en-us/articles/360040720412>`_. If your bot is already verified and you would like to enable a privileged intent you must go through `Discord support <https://dis.gd/contact>`_ and talk to them about it.
.. note:: .. note::
@ -203,4 +203,4 @@ On Windows use ``py -3`` instead of ``python3``.
There is no currently set date in which the old gateway will stop working so it is recommended to update your code instead. There is no currently set date in which the old gateway will stop working so it is recommended to update your code instead.
If you truly dislike the direction Discord is going with their API, you can contact them via `support <https://dis.gd/contact>`_ If you truly dislike the direction Discord is going with their API, you can contact them via `support <https://dis.gd/contact>`_.

View File

@ -52,7 +52,7 @@ Virtual Environments
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Sometimes you want to keep libraries from polluting system installs or use a different version of Sometimes you want to keep libraries from polluting system installs or use a different version of
libraries than the ones installed on the system. You might also not have permissions to install libaries system-wide. libraries than the ones installed on the system. You might also not have permissions to install libraries system-wide.
For this purpose, the standard library as of Python 3.3 comes with a concept called "Virtual Environment"s to For this purpose, the standard library as of Python 3.3 comes with a concept called "Virtual Environment"s to
help maintain these separate versions. help maintain these separate versions.

View File

@ -11,7 +11,7 @@ if you don't check the :ref:`installing` portion.
A Minimal Bot A Minimal Bot
--------------- ---------------
Let's make a bot that replies to a specific message and walk you through it. Let's make a bot that responds to a specific message and walk you through it.
It looks something like this: It looks something like this:
@ -53,7 +53,7 @@ There's a lot going on here, so let's walk you through it step by step.
sure that we ignore messages from ourselves. We do this by checking if the :attr:`Message.author` sure that we ignore messages from ourselves. We do this by checking if the :attr:`Message.author`
is the same as the :attr:`Client.user`. is the same as the :attr:`Client.user`.
5. Afterwards, we check if the :class:`Message.content` starts with ``'$hello'``. If it is, 5. Afterwards, we check if the :class:`Message.content` starts with ``'$hello'``. If it is,
then we reply in the channel it was used in with ``'Hello!'``. then we send a message in the channel it was used in with ``'Hello!'``.
6. Finally, we run the bot with our login token. If you need help getting your token or creating a bot, 6. Finally, we run the bot with our login token. If you need help getting your token or creating a bot,
look in the :ref:`discord-intro` section. look in the :ref:`discord-intro` section.

View File

@ -66,6 +66,78 @@ New Features
- |commands| Add :attr:`Context.clean_prefix <ext.commands.Context>` - |commands| Add :attr:`Context.clean_prefix <ext.commands.Context>`
.. _vp1p6p0:
v1.6.0
--------
This version comes with support for replies and stickers.
New Features
~~~~~~~~~~~~~~
- An entirely redesigned documentation. This was the cumulation of multiple months of effort.
- There's now a dark theme, feel free to navigate to the cog on the screen to change your setting, though this should be automatic.
- Add support for :meth:`AppInfo.icon_url_as` and :meth:`AppInfo.cover_image_url_as` (:issue:`5888`)
- Add :meth:`Colour.random` to get a random colour (:issue:`6067`)
- Add support for stickers via :class:`Sticker` (:issue:`5946`)
- Add support for replying via :meth:`Message.reply` (:issue:`6061`)
- This also comes with the :attr:`AllowedMentions.replied_user` setting.
- :meth:`abc.Messageable.send` can now accept a :class:`MessageReference`.
- :class:`MessageReference` can now be constructed by users.
- :meth:`Message.to_reference` can now convert a message to a :class:`MessageReference`.
- Add support for getting the replied to resolved message through :attr:`MessageReference.resolved`.
- Add support for role tags.
- :attr:`Guild.premium_subscriber_role` to get the "Nitro Booster" role (if available).
- :attr:`Guild.self_role` to get the bot's own role (if available).
- :attr:`Role.tags` to get the role's tags.
- :meth:`Role.is_premium_subscriber` to check if a role is the "Nitro Booster" role.
- :meth:`Role.is_bot_managed` to check if a role is a bot role (i.e. the automatically created role for bots).
- :meth:`Role.is_integration` to check if a role is role created by an integration.
- Add :meth:`Client.is_ws_ratelimited` to check if the websocket is rate limited.
- :meth:`ShardInfo.is_ws_ratelimited` is the equivalent for checking a specific shard.
- Add support for chunking an :class:`AsyncIterator` through :meth:`AsyncIterator.chunk` (:issue:`6100`, :issue:`6082`)
- Add :attr:`PartialEmoji.created_at` (:issue:`6128`)
- Add support for editing and deleting webhook sent messages (:issue:`6058`)
- This adds :class:`WebhookMessage` as well to power this behaviour.
- Add :class:`PartialMessage` to allow working with a message via channel objects and just a message_id (:issue:`5905`)
- This is useful if you don't want to incur an extra API call to fetch the message.
- Add :meth:`Emoji.url_as` (:issue:`6162`)
- Add support for :attr:`Member.pending` for the membership gating feature.
- Allow ``colour`` parameter to take ``int`` in :meth:`Guild.create_role` (:issue:`6195`)
- Add support for ``presences`` in :meth:`Guild.query_members` (:issue:`2354`)
- |commands| Add support for ``description`` keyword argument in :class:`commands.Cog <ext.commands.Cog>` (:issue:`6028`)
- |tasks| Add support for calling the wrapped coroutine as a function via ``__call__``.
Bug Fixes
~~~~~~~~~~~
- Raise :exc:`DiscordServerError` when reaching 503s repeatedly (:issue:`6044`)
- Fix :exc:`AttributeError` when :meth:`Client.fetch_template` is called (:issue:`5986`)
- Fix errors when playing audio and moving to another channel (:issue:`5953`)
- Fix :exc:`AttributeError` when voice channels disconnect too fast (:issue:`6039`)
- Fix stale :class:`User` references when the members intent is off.
- Fix :func:`on_user_update` not dispatching in certain cases when a member is not cached but the user somehow is.
- Fix :attr:`Message.author` being overwritten in certain cases during message update.
- This would previously make it so :attr:`Message.author` is a :class:`User`.
- Fix :exc:`UnboundLocalError` for editing ``public_updates_channel`` in :meth:`Guild.edit` (:issue:`6093`)
- Fix uninitialised :attr:`CustomActivity.created_at` (:issue:`6095`)
- |commands| Errors during cog unload no longer stops module cleanup (:issue:`6113`)
- |commands| Properly cleanup lingering commands when a conflicting alias is found when adding commands (:issue:`6217`)
Miscellaneous
~~~~~~~~~~~~~~~
- ``ffmpeg`` spawned processes no longer open a window in Windows (:issue:`6038`)
- Update dependencies to allow the library to work on Python 3.9+ without requiring build tools. (:issue:`5984`, :issue:`5970`)
- Fix docstring issue leading to a SyntaxError in 3.9 (:issue:`6153`)
- Update Windows opus binaries from 1.2.1 to 1.3.1 (:issue:`6161`)
- Allow :meth:`Guild.create_role` to accept :class:`int` as the ``colour`` parameter (:issue:`6195`)
- |commands| :class:`MessageConverter <ext.commands.MessageConverter>` regex got updated to support ``www.`` prefixes (:issue:`6002`)
- |commands| :class:`UserConverter <ext.commands.UserConverter>` now fetches the API if an ID is passed and the user is not cached.
- |commands| :func:`max_concurrency <ext.commands.max_concurrency>` is now called before cooldowns (:issue:`6172`)
.. _vp1p5p1: .. _vp1p5p1:
v1.5.1 v1.5.1

View File

@ -44,7 +44,7 @@ class RoleReactClient(discord.Client):
async def on_raw_reaction_remove(self, payload): async def on_raw_reaction_remove(self, payload):
"""Removes a role based on a reaction emoji.""" """Removes a role based on a reaction emoji."""
# Make sure that the message the user is reacting to is the one we care about # Make sure that the message the user is reacting to is the one we care about
if payload.message_id == self.role_message_id: if payload.message_id != self.role_message_id:
return return
try: try: