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')
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())

View File

@ -758,7 +758,7 @@ class GuildChannel:
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
-------

View File

@ -153,6 +153,19 @@ class Asset:
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):
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 .asset import Asset
from .errors import ClientException, NoMoreItems, InvalidArgument
from .webhook import Webhook
__all__ = (
'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`.
type: :class:`ChannelType`
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`.
reason: Optional[:class:`str`]
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.
"""
from .webhook import Webhook
data = await self._state.http.channel_webhooks(self.id)
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.
"""
from .webhook import Webhook
if avatar is not None:
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):
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)
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):
"""Represents a Discord guild voice channel.
@ -1079,6 +1103,28 @@ class DMChannel(discord.abc.Messageable, Hashable):
base.manage_messages = False
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):
"""Represents a Discord group channel.

View File

@ -134,10 +134,12 @@ class Emoji(_EmojiTag):
@property
def url(self):
""":class:`Asset`: Returns the asset of the emoji."""
_format = 'gif' if self.animated else 'png'
url = "/emojis/{0.id}.{1}".format(self, _format)
return Asset(self._state, url)
""":class:`Asset`: Returns the asset of the emoji.
This is equivalent to calling :meth:`url_as` with
the default parameters (i.e. png/gif detection).
"""
return self.url_as(format=None)
@property
def roles(self):
@ -156,6 +158,39 @@ class Emoji(_EmojiTag):
""":class:`Guild`: The guild this emoji belongs to."""
return self._state._get_guild(self.guild_id)
def url_as(self, *, format=None, static_format="png"):
"""Returns an :class:`Asset` for the emoji's url.
The format must be one of 'webp', 'jpeg', 'jpg', 'png' or 'gif'.
'gif' is only valid for animated emojis.
.. versionadded:: 1.6
Parameters
-----------
format: Optional[:class:`str`]
The format to attempt to convert the emojis to.
If the format is ``None``, then it is automatically
detected as either 'gif' or static_format, depending on whether the
emoji is animated or not.
static_format: Optional[:class:`str`]
Format to attempt to convert only non-animated emoji's to.
Defaults to 'png'
Raises
-------
InvalidArgument
Bad image format passed to ``format`` or ``static_format``.
Returns
--------
:class:`Asset`
The resulting CDN asset.
"""
return Asset._from_emoji(self._state, self, format=format, static_format=static_format)
def is_usable(self):
""":class:`bool`: Whether the bot can use this emoji.

View File

@ -263,7 +263,7 @@ class MessageConverter(Converter):
3. Lookup by message URL
.. 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):
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.
brief: Optional[:class:`str`]
The short help text for the command.
usage: :class:`str`
usage: Optional[:class:`str`]
A replacement for arguments in the default help text.
aliases: Union[List[:class:`str`], Tuple[:class:`str`]]
The list of aliases the command can be invoked under.
@ -778,17 +778,22 @@ class Command(_BaseCommand):
if not await self.can_run(ctx):
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:
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):
"""Checks whether the command is currently on cooldown.
@ -1140,6 +1145,7 @@ class GroupMixin:
self.all_commands[command.name] = command
for alias in command.aliases:
if alias in self.all_commands:
self.remove_command(command.name)
raise CommandRegistrationError(alias, alias_conflict=True)
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.
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
def walk_commands(self):

View File

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

View File

@ -160,6 +160,26 @@ class Loop:
return None
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):
r"""Starts the internal task in the event loop.

View File

@ -28,7 +28,7 @@ import os.path
import io
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.
.. note::

View File

@ -636,11 +636,12 @@ class DiscordWebSocket:
}
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 = {
'op': self.REQUEST_MEMBERS,
'd': {
'guild_id': guild_id,
'presences': presences,
'limit': limit
}
}

View File

@ -41,7 +41,6 @@ from .mixins import Hashable
from .user import User
from .invite import Invite
from .iterators import AuditLogIterator, MemberIterator
from .webhook import Webhook
from .widget import Widget
from .asset import Asset
from .flags import SystemChannelFlags
@ -145,6 +144,8 @@ class Guild(Hashable):
- ``ANIMATED_ICON``: Guild can upload an animated icon.
- ``PUBLIC_DISABLED``: Guild cannot be public.
- ``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`]
The guild's invite splash.
@ -485,7 +486,7 @@ class Guild(Hashable):
@property
def public_updates_channel(self):
"""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.
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.
system_channel_flags: :class:`SystemChannelFlags`
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`]
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
@ -1535,6 +1539,7 @@ class Guild(Hashable):
The webhooks for this guild.
"""
from .webhook import Webhook
data = await self._state.http.guild_webhooks(self.id)
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'.
permissions: :class:`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`.
This is aliased to ``color`` as well.
hoist: :class:`bool`
@ -1826,6 +1831,8 @@ class Guild(Hashable):
except KeyError:
colour = fields.get('color', Colour.default())
finally:
if isinstance(colour, int):
colour = Colour(value=colour)
fields['color'] = colour.value
valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable')
@ -2157,7 +2164,7 @@ class Guild(Hashable):
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|
Request members that belong to this guild whose username starts with
@ -2174,6 +2181,12 @@ class Guild(Hashable):
limit: :class:`int`
The maximum number of members to send back. This must be
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`
Whether to cache the members internally. This makes operations
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.
ValueError
Invalid parameters were passed to the function
ClientException
The presences intent is not enabled.
Returns
--------
@ -2196,6 +2211,9 @@ class Guild(Hashable):
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 == '':
raise ValueError('Cannot pass empty query string.')
@ -2207,7 +2225,7 @@ class Guild(Hashable):
raise ValueError('Cannot pass both query and user_ids')
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):
"""|coro|

View File

@ -659,7 +659,7 @@ class HTTPClient:
'system_channel_id', 'default_message_notifications',
'description', 'explicit_content_filter', 'banner',
'system_channel_flags', 'rules_channel_id',
'public_updates_channel_id')
'public_updates_channel_id', 'preferred_locale',)
payload = {
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.
nick: Optional[:class:`str`]
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`]
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``.
"""
__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):
self._state = state
@ -177,6 +181,7 @@ class Member(discord.abc.Messageable, _BaseUser):
}
self.activities = tuple(map(create_activity, data.get('activities', [])))
self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
def __str__(self):
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._update_roles(data)
self.nick = data.get('nick', None)
self.pending = data.get('pending', False)
@classmethod
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.guild = member.guild
self.nick = member.nick
self.pending = member.pending
self.activities = member.activities
self._state = member._state
@ -264,6 +271,11 @@ class Member(discord.abc.Messageable, _BaseUser):
except KeyError:
pass
try:
self.pending = data['pending']
except KeyError:
pass
self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data)
@ -634,7 +646,8 @@ class Member(discord.abc.Messageable, _BaseUser):
Gives the member a number of :class:`Role`\s.
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
-----------
@ -672,7 +685,8 @@ class Member(discord.abc.Messageable, _BaseUser):
Removes :class:`Role`\s from this member.
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
-----------

View File

@ -34,7 +34,7 @@ from .reaction import Reaction
from .emoji import Emoji
from .partial_emoji import PartialEmoji
from .calls import CallMessage
from .enums import MessageType, try_enum
from .enums import MessageType, ChannelType, try_enum
from .errors import InvalidArgument, ClientException, HTTPException
from .embeds import Embed
from .member import Member
@ -48,10 +48,26 @@ from .sticker import Sticker
__all__ = (
'Attachment',
'Message',
'PartialMessage',
'MessageReference',
'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:
"""Represents an attachment from Discord.
@ -268,7 +284,7 @@ class MessageReference:
The guild id of the message referenced.
resolved: Optional[Union[:class:`Message`, :class:`DeletedReferencedMessage`]]
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.
If the message was resolved at a prior point but has since been deleted then
this will be of type :class:`DeletedReferencedMessage`.
@ -372,7 +388,19 @@ def flatten_handlers(cls):
class Message(Hashable):
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
-----------
@ -423,7 +451,7 @@ class Message(Hashable):
.. warning::
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`]
A list of :class:`abc.GuildChannel` that were mentioned. If the message is in a private message
then the list is always empty.
@ -996,14 +1024,6 @@ class Message(Hashable):
are used instead.
.. 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
-------
@ -1041,24 +1061,17 @@ class Message(Hashable):
delete_after = fields.pop('delete_after', None)
mention_author = fields.pop('mention_author', None)
allowed_mentions = fields.pop('allowed_mentions', None)
if allowed_mentions is not None:
if self._state.allowed_mentions is not None:
allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions)
allowed_mentions = allowed_mentions.to_dict()
if mention_author is not None:
allowed_mentions['replied_user'] = mention_author
fields['allowed_mentions'] = allowed_mentions
elif mention_author is not None:
if self._state.allowed_mentions is not None:
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()
try:
allowed_mentions = fields.pop('allowed_mentions')
except KeyError:
pass
else:
if allowed_mentions is not None:
if self._state.allowed_mentions is not None:
allowed_mentions = self._state.allowed_mentions.merge(allowed_mentions).to_dict()
else:
allowed_mentions = allowed_mentions.to_dict()
fields['allowed_mentions'] = allowed_mentions
if 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.
"""
emoji = self._emoji_reaction(emoji)
emoji = convert_emoji_reaction(emoji)
await self._state.http.add_reaction(self.channel.id, self.id, emoji)
async def remove_reaction(self, emoji, member):
@ -1205,7 +1218,7 @@ class Message(Hashable):
The emoji parameter is invalid.
"""
emoji = self._emoji_reaction(emoji)
emoji = convert_emoji_reaction(emoji)
if member.id == self._state.self_id:
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.
"""
emoji = self._emoji_reaction(emoji)
emoji = convert_emoji_reaction(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):
"""|coro|
@ -1301,7 +1298,7 @@ class Message(Hashable):
A shortcut method to :meth:`abc.Messageable.send` to reply to the
:class:`Message`.
.. versionadded:: 1.6
.. versionadded:: 1.6
Raises
--------
@ -1344,3 +1341,116 @@ class Message(Hashable):
data['guild_id'] = self.guild.id
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.util
import logging
import math
import os.path
import struct
import sys
from .errors import DiscordException
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_float_ptr = ctypes.POINTER(ctypes.c_float)
@ -43,17 +46,55 @@ _lib = None
class EncoderStruct(ctypes.Structure):
pass
class DecoderStruct(ctypes.Structure):
pass
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):
if result < 0:
if result < OK:
log.info('error has happened in %s', func.__name__)
raise OpusError(result)
return result
def _err_ne(result, func, args):
ret = args[-1]._obj
if ret.value != 0:
if ret.value != OK:
log.info('error has happened in %s', func.__name__)
raise OpusError(ret.value)
return result
@ -64,18 +105,53 @@ def _err_ne(result, func, args):
# The third is the result type.
# The fourth is the error handler.
exported_functions = [
# Generic
('opus_get_version_string',
None, ctypes.c_char_p, None),
('opus_strerror',
[ctypes.c_int], ctypes.c_char_p, None),
# Encoder functions
('opus_encoder_get_size',
[ctypes.c_int], ctypes.c_int, None),
('opus_encoder_create',
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne),
('opus_encode',
[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',
None, ctypes.c_int32, _err_lt),
('opus_encoder_destroy',
[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):
@ -107,8 +183,9 @@ def _load_default():
try:
if sys.platform == 'win32':
_basedir = os.path.dirname(os.path.abspath(__file__))
_bitness = 'x64' if sys.maxsize > 2**32 else 'x86'
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_bitness))
_bitness = struct.calcsize('P') * 8
_target = 'x64' if _bitness > 32 else 'x86'
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target))
_lib = libopus_loader(_filename)
else:
_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."""
pass
# 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:
class _OpusStruct:
SAMPLING_RATE = 48000
CHANNELS = 2
FRAME_LENGTH = 20
SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16)
FRAME_LENGTH = 20 # in milliseconds
SAMPLE_SIZE = struct.calcsize('h') * CHANNELS
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
def __init__(self, application=APPLICATION_AUDIO):
self.application = application
@staticmethod
def get_opus_version() -> str:
if not is_loaded():
if not _load_default():
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.set_bitrate(128)
self.set_fec(True)
@ -280,3 +339,84 @@ class Encoder:
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
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 '%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
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():
return Asset(self._state)

View File

@ -382,11 +382,11 @@ class ConnectionState:
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
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
ws = self._get_websocket(guild_id)
if ws is None:
@ -397,7 +397,7 @@ class ConnectionState:
try:
# 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)
except asyncio.TimeoutError:
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'])
return
channel = None
if ch_type in (ChannelType.group, ChannelType.private):
channel_id = int(data['id'])
if self._get_private_channel(channel_id) is None:
@ -795,6 +793,12 @@ class ConnectionState:
else:
if self.member_cache_flags.joined:
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)
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):
guild_id = int(data['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', [])]
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')
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)
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)
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):
await self.shards_launched.wait()

View File

@ -35,6 +35,7 @@ import aiohttp
from . import utils
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound, DiscordServerError
from .message import Message
from .enums import try_enum, WebhookType
from .user import BaseUser, User
from .asset import Asset
@ -45,6 +46,7 @@ __all__ = (
'AsyncWebhookAdapter',
'RequestsWebhookAdapter',
'Webhook',
'WebhookMessage',
)
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.webhook = webhook
def is_async(self):
return False
def request(self, verb, url, payload=None, multipart=None):
"""Actually does the request.
@ -94,6 +99,12 @@ class WebhookAdapter:
def edit_webhook(self, *, reason=None, **payload):
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):
"""Transforms the webhook execution response into something
more meaningful.
@ -178,6 +189,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
self.session = session
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):
headers = {}
data = None
@ -253,8 +267,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
return data
# transform into Message object
from .message import Message
return Message(data=data, state=self.webhook._state, channel=self.webhook.channel)
# Make sure to coerce the state to the partial one to allow message edits/delete
state = _PartialWebhookState(self, self.webhook, parent=self.webhook._state)
return WebhookMessage(data=data, state=state, channel=self.webhook.channel)
class RequestsWebhookAdapter(WebhookAdapter):
"""A webhook adapter suited for use with ``requests``.
@ -356,8 +371,9 @@ class RequestsWebhookAdapter(WebhookAdapter):
return response
# transform into Message object
from .message import Message
return Message(data=response, state=self.webhook._state, channel=self.webhook.channel)
# Make sure to coerce the state to the partial one to allow message edits/delete
state = _PartialWebhookState(self, self.webhook, parent=self.webhook._state)
return WebhookMessage(data=response, state=state, channel=self.webhook.channel)
class _FriendlyHttpAttributeErrorHelper:
__slots__ = ()
@ -366,9 +382,16 @@ class _FriendlyHttpAttributeErrorHelper:
raise AttributeError('PartialWebhookState does not support http methods.')
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
try:
self.loop = adapter.loop
@ -387,13 +410,111 @@ class _PartialWebhookState:
@property
def http(self):
if self.parent is not None:
return self.parent.http
# Some data classes assign state.http and that should be kosher
# however, using it should result in a late-binding error.
return _FriendlyHttpAttributeErrorHelper()
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))
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):
"""Represents a Discord webhook.
@ -488,7 +609,7 @@ class Webhook(Hashable):
self.name = data.get('name')
self.avatar = data.get('avatar')
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._prepare(self)
@ -785,7 +906,7 @@ class Webhook(Hashable):
wait: :class:`bool`
Whether the server should wait before sending a response. This essentially
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`
The username to send with this message. If no username is provided
then the default username for the webhook is used.
@ -825,7 +946,7 @@ class Webhook(Hashable):
Returns
---------
Optional[:class:`Message`]
Optional[:class:`WebhookMessage`]
The message that was sent.
"""
@ -869,3 +990,115 @@ class Webhook(Hashable):
def execute(self, *args, **kwargs):
"""An alias for :meth:`~.Webhook.send`."""
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 .ne { color: #bbbbbb; } /* Name.Exception */
: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 .nn { color: #6494d8;} /* Name.Namespace */
:root[data-theme="dark"] .highlight .nx { color: #d0d0d0; } /* Name.Other */

View File

@ -5,6 +5,7 @@ let bottomHeightThreshold, sections;
let hamburgerToggle;
let mobileSearch;
let sidebar;
let toTop;
class Modal {
constructor(element) {
@ -49,12 +50,19 @@ class SearchBar {
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.addEventListener('DOMContentLoaded', () => {
mobileSearch = new SearchBar();
bottomHeightThreshold = document.documentElement.scrollHeight - 30;
sections = document.querySelectorAll('section');
hamburgerToggle = document.getElementById('hamburger-toggle');
toTop = document.getElementById('to-top');
toTop.hidden = !(window.scrollY > 0);
if (hamburgerToggle) {
hamburgerToggle.addEventListener('click', (e) => {
@ -76,6 +84,16 @@ document.addEventListener('DOMContentLoaded', () => {
// insert ourselves after the element
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) => {

View File

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

View File

@ -19,6 +19,7 @@ Historically however, thanks to:
/* CSS variables would go here */
:root {
--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 */
--white: #ffffff;
@ -96,6 +97,8 @@ Historically however, thanks to:
--rtd-ad-background: var(--grey-2);
--rtd-ad-main-text: var(--grey-6);
--rtd-ad-small-text: var(--grey-4);
--rtd-version-background: #272525;
--rtd-version-main-text: #fcfcfc;
--attribute-table-title: var(--grey-6);
--attribute-table-entry-border: var(--grey-3);
--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-text: var(--blue-2);
--attribute-table-badge: var(--grey-7);
--highlighted-text: rgb(252, 233, 103);
}
:root[data-font="serif"] {
@ -162,6 +166,7 @@ Historically however, thanks to:
--attribute-table-entry-hover-background: var(--grey-6);
--attribute-table-entry-hover-text: var(--blue-1);
--attribute-table-badge: var(--grey-4);
--highlighted-text: rgba(250, 166, 26, 0.2);
}
img[src$="snake_dark.svg"] {
@ -247,6 +252,7 @@ header > nav {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
header > nav a {
@ -265,6 +271,12 @@ header > nav.mobile-only {
header > nav.mobile-only .search {
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 {
@ -316,6 +328,11 @@ header > nav > a:hover {
cursor: pointer;
}
.sub-header option {
color: black;
}
.sub-header > select:focus {
outline: none;
}
@ -380,12 +397,12 @@ aside h3 {
position: relative;
line-height: 0.5em;
transition: transform 0.4s;
transform: rotate(0deg);
transform: rotate(-90deg);
}
.expanded {
transition: transform 0.4s;
transform: rotate(-90deg);
transform: rotate(0deg);
}
.ref-internal-padding {
@ -567,6 +584,37 @@ div.modal input {
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 */
.relative-copy {
@ -855,7 +903,7 @@ dl.field-list {
/* internal references are forced to bold for some reason */
a.reference.internal > strong {
font-weight: unset;
font-family: monospace;
font-family: var(--monospace-font-family);
}
/* exception hierarchy */
@ -950,7 +998,7 @@ pre {
}
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;
overflow-wrap: break-word;
}
@ -1007,6 +1055,13 @@ dd {
margin-left: 1.5em;
}
dt:target, span.highlighted {
background-color: var(--highlighted-text);
}
rect.highlighted {
fill: var(--highlighted-text);
}
.container.operations {
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>
</nav>
<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">
<input type="search" name="q" placeholder="{{ _('Search documentation') }}" />
<button type="submit">
@ -90,7 +90,7 @@
<option value="{{ pathto(p + '/index')|e }}" {% if pagename is prefixedwith p %}selected{% endif %}>{{ ext }}</option>
{%- endfor %}
</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">
<input type="search" name="q" placeholder="{{ _('Search documentation') }}" />
<button type="submit">
@ -115,7 +115,7 @@
</div>
</aside>
{#- The actual body of the contents #}
<main class="grid-item">
<main class="grid-item" role="main">
{% block body %} {% endblock %}
</main>
{%- block footer %}
@ -190,5 +190,9 @@
</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>
</html>

View File

@ -87,6 +87,14 @@ VoiceClient
.. autoclass:: VoiceClient()
:members:
VoiceProtocol
~~~~~~~~~~~~~~~
.. attributetable:: VoiceProtocol
.. autoclass:: VoiceProtocol
:members:
AudioSource
~~~~~~~~~~~~
@ -147,7 +155,7 @@ Opus Library
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
: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
- nickname
- roles
- pending
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.
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.
.. versionadded:: 1.6
Collecting groups of users: ::
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.
Webhook
~~~~~~~~~
.. attributetable:: Webhook
.. autoclass:: Webhook
:members:
WebhookMessage
~~~~~~~~~~~~~~~~
.. attributetable:: WebhookMessage
.. autoclass:: WebhookMessage
:members:
Adapters
~~~~~~~~~
@ -2790,6 +2810,8 @@ Message
DeletedReferencedMessage
~~~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: DeletedReferencedMessage
.. autoclass:: DeletedReferencedMessage()
:members:
@ -2998,6 +3020,8 @@ Invite
Template
~~~~~~~~~
.. attributetable:: Template
.. autoclass:: Template()
:members:
@ -3029,6 +3053,8 @@ Widget
Sticker
~~~~~~~~~~~~~~~
.. attributetable:: Sticker
.. autoclass:: Sticker()
:members:
@ -3101,6 +3127,8 @@ dynamic attributes in mind.
Object
~~~~~~~
.. attributetable:: Object
.. autoclass:: Object
:members:
@ -3126,6 +3154,12 @@ MessageReference
.. autoclass:: MessageReference
:members:
PartialMessage
~~~~~~~~~~~~~~~~~
.. autoclass:: PartialMessage
:members:
Intents
~~~~~~~~~~

View File

@ -40,6 +40,7 @@ extensions = [
'details',
'exception_hierarchy',
'attributetable',
'resourcelinks',
]
autodoc_member_order = 'bysource'
@ -76,7 +77,7 @@ master_doc = 'index'
# General information about the project.
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
# |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.
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
# 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
# further. For a list of options available for each theme, see the
# documentation.
@ -337,3 +348,4 @@ def setup(app):
if app.config.language == 'ja':
app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None)
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.
- **This is not the Client Secret at the General Information page**
- **This is not the Client Secret at the General Information page.**
.. warning::

View File

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

View File

@ -135,6 +135,8 @@ Doing something during cancellation:
API Reference
---------------
.. attributetable:: discord.ext.tasks.Loop
.. autoclass:: discord.ext.tasks.Loop()
: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
- Optimised for both speed and memory
Documentation Contents
-----------------------
Getting started
-----------------
.. toctree::
:maxdepth: 2
Is this your first time using the library? This is the place to get started!
intro
quickstart
migrating
logging
api
- **First steps:** :doc:`intro` | :doc:`quickstart` | :doc:`logging`
- **Working with Discord:** :doc:`discord` | :doc:`intents`
- **Examples:** Many examples are available in the :resource:`repository <examples>`.
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
-----------
------------
These extensions help you during development when it comes to common tasks.
.. toctree::
:maxdepth: 3
:maxdepth: 1
ext/commands/index.rst
ext/tasks/index.rst
Manuals
---------
Additional Information
-----------------------
These pages go into great detail about everything the API can do.
.. toctree::
:maxdepth: 2
:maxdepth: 1
discord
intents
faq
whats_new
version_guarantees
api
discord.ext.commands API Reference <ext/commands/api.rst>
discord.ext.tasks API Reference <ext/tasks/index.rst>
If you still can't find what you're looking for, try in one of the following pages:
Meta
------
* :ref:`genindex`
* :ref:`search`
If you're looking for something related to the project itself, it's here.
.. 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:
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.
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::
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::
@ -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.
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
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
help maintain these separate versions.

View File

@ -11,7 +11,7 @@ if you don't check the :ref:`installing` portion.
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:
@ -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`
is the same as the :attr:`Client.user`.
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,
look in the :ref:`discord-intro` section.

View File

@ -66,6 +66,78 @@ New Features
- |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:
v1.5.1

View File

@ -44,7 +44,7 @@ class RoleReactClient(discord.Client):
async def on_raw_reaction_remove(self, payload):
"""Removes a role based on a reaction emoji."""
# 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
try: