conflict fixes
This commit is contained in:
commit
86fd3fb738
@ -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())
|
||||
|
@ -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
|
||||
-------
|
||||
|
@ -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.
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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})$')
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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::
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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|
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
-----------
|
||||
|
@ -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)
|
||||
|
214
discord/opus.py
214
discord/opus.py
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
1
docs/_static/codeblocks.css
vendored
1
docs/_static/codeblocks.css
vendored
@ -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 */
|
||||
|
18
docs/_static/custom.js
vendored
18
docs/_static/custom.js
vendored
@ -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) => {
|
||||
|
5
docs/_static/settings.js
vendored
5
docs/_static/settings.js
vendored
@ -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();
|
||||
}
|
||||
});
|
||||
|
63
docs/_static/style.css
vendored
63
docs/_static/style.css
vendored
@ -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;
|
||||
|
10
docs/_templates/layout.html
vendored
10
docs/_templates/layout.html
vendored
@ -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>
|
||||
|
40
docs/api.rst
40
docs/api.rst
@ -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
|
||||
~~~~~~~~~~
|
||||
|
||||
|
14
docs/conf.py
14
docs/conf.py
@ -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'
|
@ -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::
|
||||
|
||||
|
@ -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::
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -135,6 +135,8 @@ Doing something during cancellation:
|
||||
API Reference
|
||||
---------------
|
||||
|
||||
.. attributetable:: discord.ext.tasks.Loop
|
||||
|
||||
.. autoclass:: discord.ext.tasks.Loop()
|
||||
:members:
|
||||
|
||||
|
44
docs/extensions/resourcelinks.py
Normal file
44
docs/extensions/resourcelinks.py
Normal 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}
|
@ -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
|
||||
|
@ -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>`_.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user