This commit is contained in:
iDutchy 2020-09-28 00:42:27 +00:00
commit 64be57b192
38 changed files with 903 additions and 231 deletions

View File

@ -1,26 +1,27 @@
---
name: Bug Report
about: Report broken or incorrect behaviour
labels: bug
---
### Summary
## Summary
<!-- A summary of your bug report -->
### Reproduction Steps
## Reproduction Steps
<!-- What you did to make it happen. Ideally there should be a short code snippet in this section to help reproduce the bug. -->
### Expected Results
## Expected Results
<!-- What you expected to happen -->
### Actual Results
## Actual Results
<!-- What actually happened. If there is a traceback, please show the entire thing. -->
### Checklist
## Checklist
<!-- Put an x inside [ ] to check it, like so: [x] -->
@ -28,7 +29,7 @@ about: Report broken or incorrect behaviour
- [ ] I have shown the entire traceback, if possible.
- [ ] I have removed my token from display, if visible.
### System Information
## System Information
<!-- Run `python -m discord -v` and paste this information below. -->
<!-- This command is available in v1.1.0 or higher. If you are unable to run this command, paste basic info (ie. Python version, library version, and your operating system -->

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question about the library
about: Support questions are better answered in our Discord server. Issues asking how to implement a feature in your bot will be closed.
- name: Ask a question
about: Ask questions and discuss with other users of the library.
url: https://github.com/Rapptz/discord.py/discussions
- name: Discord Server
about: Use our official Discord server to ask help and questions as well.
url: https://discord.gg/r3sSKJJ

View File

@ -1,26 +1,27 @@
---
name: Feature Request
about: Suggest a feature for this library
labels: feature request
---
### The Problem
## The Problem
<!--
What problem is your feature trying to solve? What becomes easier or possible when this feature is implemented?
-->
### The Ideal Solution
## The Ideal Solution
<!--
What is your ideal solution to the problem? What would you like this feature to do?
-->
### The Current Solution
## The Current Solution
<!--
What is the current solution to the problem, if any?
-->
### Summary
## Summary
<!-- A short summary of your feature request. -->

View File

@ -1,8 +1,8 @@
### Summary
## Summary
<!-- What is this pull request for? Does it fix any issues? -->
### Checklist
## Checklist
<!-- Put an x inside [ ] to check it, like so: [x] -->

View File

@ -41,7 +41,7 @@ Otherwise to get voice support you should run the following command:
.. code:: sh
# Linux/macOS
python3 -m pip install -U discord.py[voice]
python3 -m pip install -U "discord.py[voice]"
# Windows
py -3 -m pip install -U discord.py[voice]

View File

@ -33,7 +33,7 @@ from .guild import Guild
from .flags import *
from .relationship import Relationship
from .member import Member, VoiceState
from .message import Message, Attachment
from .message import Message, MessageReference, Attachment
from .asset import Asset
from .errors import *
from .calls import CallMessage, GroupCall
@ -54,7 +54,7 @@ from .mentions import AllowedMentions
from .shard import AutoShardedClient, ShardInfo
from .player import *
from .webhook import *
from .voice_client import VoiceClient
from .voice_client import VoiceClient, VoiceProtocol
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
from .raw_models import *
from .team import *

View File

@ -37,7 +37,7 @@ from .permissions import PermissionOverwrite, Permissions
from .role import Role
from .invite import Invite
from .file import File
from .voice_client import VoiceClient
from .voice_client import VoiceClient, VoiceProtocol
from . import utils
class _Undefined:
@ -699,6 +699,11 @@ class GuildChannel:
You do not have the proper permissions to create this channel.
~discord.HTTPException
Creating the channel failed.
Returns
--------
:class:`.abc.GuildChannel`
The channel that was created.
"""
raise NotImplementedError
@ -878,7 +883,7 @@ class Messageable(metaclass=abc.ABCMeta):
raise InvalidArgument('file parameter must be File')
try:
data = await state.http.send_files(channel.id, files=[file],
data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions,
content=content, tts=tts, embed=embed, nonce=nonce)
finally:
file.close()
@ -1049,7 +1054,6 @@ class Messageable(metaclass=abc.ABCMeta):
"""
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
class Connectable(metaclass=abc.ABCMeta):
"""An ABC that details the common operations on a channel that can
connect to a voice server.
@ -1068,7 +1072,7 @@ class Connectable(metaclass=abc.ABCMeta):
def _get_voice_state_pair(self):
raise NotImplementedError
async def connect(self, *, timeout=60.0, reconnect=True):
async def connect(self, *, timeout=60.0, reconnect=True, cls=VoiceClient):
"""|coro|
Connects to voice and creates a :class:`VoiceClient` to establish
@ -1082,6 +1086,9 @@ class Connectable(metaclass=abc.ABCMeta):
Whether the bot should automatically attempt
a reconnect if a part of the handshake fails
or the gateway goes down.
cls: Type[:class:`VoiceProtocol`]
A type that subclasses :class:`~discord.VoiceProtocol` to connect with.
Defaults to :class:`~discord.VoiceClient`.
Raises
-------
@ -1094,20 +1101,25 @@ class Connectable(metaclass=abc.ABCMeta):
Returns
--------
:class:`~discord.VoiceClient`
:class:`~discord.VoiceProtocol`
A voice client that is fully connected to the voice server.
"""
if not issubclass(cls, VoiceProtocol):
raise TypeError('Type must meet VoiceProtocol abstract base class.')
key_id, _ = self._get_voice_client_key()
state = self._state
if state._get_voice_client(key_id):
raise ClientException('Already connected to a voice channel.')
voice = VoiceClient(state=state, timeout=timeout, channel=self)
client = state._get_client()
voice = cls(client, self)
state._add_voice_client(key_id, voice)
try:
await voice.connect(reconnect=reconnect)
await voice.connect(timeout=timeout, reconnect=reconnect)
except asyncio.TimeoutError:
try:
await voice.disconnect(force=True)

View File

@ -158,11 +158,11 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
def is_nsfw(self):
"""Checks if the channel is NSFW."""
""":class:`bool`: Checks if the channel is NSFW."""
return self.nsfw
def is_news(self):
"""Checks if the channel is a news channel."""
""":class:`bool`: Checks if the channel is a news channel."""
return self._type == ChannelType.news.value
@property
@ -757,7 +757,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
return ChannelType.category
def is_nsfw(self):
"""Checks if the category is NSFW."""
""":class:`bool`: Checks if the category is NSFW."""
return self.nsfw
async def clone(self, *, name=None, reason=None):
@ -933,7 +933,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
def is_nsfw(self):
"""Checks if the channel is NSFW."""
""":class:`bool`: Checks if the channel is NSFW."""
return self.nsfw
async def clone(self, *, name=None, reason=None):

View File

@ -25,7 +25,6 @@ DEALINGS IN THE SOFTWARE.
"""
import asyncio
from collections import namedtuple
import logging
import signal
import sys
@ -144,17 +143,24 @@ class Client:
intents: :class:`Intents`
The intents that you want to enable for the session. This is a way of
disabling and enabling certain gateway events from triggering and being sent.
If not given, defaults to a regularly constructed :class:`Intents` class.
.. versionadded:: 1.5
member_cache_flags: :class:`MemberCacheFlags`
Allows for finer control over how the library caches members.
If not given, defaults to cache as much as possible with the
currently selected intents.
.. versionadded:: 1.5
fetch_offline_members: :class:`bool`
Indicates if :func:`.on_ready` should be delayed to fetch all offline
members from the guilds the client belongs to. If this is ``False``\, then
no offline members are received and :meth:`request_offline_members`
must be used to fetch the offline members of the guild.
A deprecated alias of ``chunk_guilds_at_startup``.
chunk_guilds_at_startup: :class:`bool`
Indicates if :func:`.on_ready` should be delayed to chunk all guilds
at start-up if necessary. This operation is incredibly slow for large
amounts of guilds. The default is ``True`` if :attr:`Intents.members`
is ``True``.
.. versionadded:: 1.5
status: Optional[:class:`.Status`]
A status to start your presence with upon logging on to Discord.
activity: Optional[:class:`.BaseActivity`]
@ -241,13 +247,12 @@ class Client:
'before_identify': self._call_before_identify_hook
}
self._connection = ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
self._connection = self._get_state(**options)
self._connection.shard_count = self.shard_count
self._closed = False
self._ready = asyncio.Event()
self._connection._get_websocket = self._get_websocket
self._connection._get_client = lambda: self
if VoiceClient.warn_nacl:
VoiceClient.warn_nacl = False
@ -258,6 +263,10 @@ class Client:
def _get_websocket(self, guild_id=None, *, shard_id=None):
return self.ws
def _get_state(self, **options):
return ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
async def _syncer(self, guilds):
await self.ws.request_sync(guilds)
@ -309,11 +318,14 @@ class Client:
@property
def voice_clients(self):
"""List[:class:`.VoiceClient`]: Represents a list of voice connections."""
"""List[:class:`.VoiceProtocol`]: Represents a list of voice connections.
These are usually :class:`.VoiceClient` instances.
"""
return self._connection.voice_clients
def is_ready(self):
"""Specifies if the client's internal cache is ready for use."""
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
return self._ready.is_set()
async def _run_event(self, coro, event_name, *args, **kwargs):
@ -701,7 +713,7 @@ class Client:
# properties
def is_closed(self):
"""Indicates if the websocket connection is closed."""
""":class:`bool`: Indicates if the websocket connection is closed."""
return self._closed
@property
@ -737,6 +749,14 @@ class Client:
else:
raise TypeError('allowed_mentions must be AllowedMentions not {0.__class__!r}'.format(value))
@property
def intents(self):
""":class:`Intents`: The intents configured for this connection.
.. versionadded:: 1.5
"""
return self._connection.intents
# helpers/getters
@property
@ -818,6 +838,11 @@ class Client:
Just because you receive a :class:`.abc.GuildChannel` does not mean that
you can communicate in said channel. :meth:`.abc.GuildChannel.permissions_for` should
be used for that.
Yields
------
:class:`.abc.GuildChannel`
A channel the client can 'access'.
"""
for guild in self.guilds:
@ -832,6 +857,11 @@ class Client:
for guild in client.guilds:
for member in guild.members:
yield member
Yields
------
:class:`.Member`
A member the client can see.
"""
for guild in self.guilds:
for member in guild.members:

View File

@ -216,7 +216,13 @@ class Cog(metaclass=CogMeta):
return cleaned
def walk_commands(self):
"""An iterator that recursively walks through this cog's commands and subcommands."""
"""An iterator that recursively walks through this cog's commands and subcommands.
Yields
------
Union[:class:`.Command`, :class:`.Group`]
A command or group from the cog.
"""
from .core import GroupMixin
for command in self.__cog_commands__:
if command.parent is None:

View File

@ -238,7 +238,7 @@ class Context(discord.abc.Messageable):
@property
def voice_client(self):
r"""Optional[:class:`.VoiceClient`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
g = self.guild
return g.voice_client if g else None

View File

@ -362,6 +362,7 @@ class CategoryChannelConverter(IDConverter):
class ColourConverter(Converter):
"""Converts to a :class:`~discord.Colour`.
.. versionchanged:: 1.5
Add an alias named ColorConverter

View File

@ -1180,6 +1180,11 @@ class GroupMixin:
.. versionchanged:: 1.4
Duplicates due to aliases are no longer returned
Yields
------
Union[:class:`.Command`, :class:`.Group`]
A command or group from the internal list of commands.
"""
for command in self.commands:
yield command
@ -1233,7 +1238,7 @@ class GroupMixin:
Returns
--------
Callable[..., :class:`Command`]
A decorator that converts the provided method into a Command, adds it to the bot, then returns it
A decorator that converts the provided method into a Command, adds it to the bot, then returns it.
"""
def decorator(func):
kwargs.setdefault('parent', self)
@ -1246,6 +1251,11 @@ class GroupMixin:
def group(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`.
Returns
--------
Callable[..., :class:`Group`]
A decorator that converts the provided method into a Group, adds it to the bot, then returns it.
"""
def decorator(func):
kwargs.setdefault('parent', self)

View File

@ -273,7 +273,7 @@ class ChannelNotReadable(BadArgument):
Attributes
-----------
argument: :class:`Channel`
argument: :class:`.abc.GuildChannel`
The channel supplied by the caller that was not readable
"""
def __init__(self, argument):
@ -403,7 +403,7 @@ class CommandInvokeError(CommandError):
Attributes
-----------
original
original: :exc:`Exception`
The original exception that was raised. You can also get this via
the ``__cause__`` attribute.
"""
@ -438,7 +438,7 @@ class MaxConcurrencyReached(CommandError):
------------
number: :class:`int`
The maximum number of concurrent invokers allowed.
per: :class:`BucketType`
per: :class:`.BucketType`
The bucket type passed to the :func:`.max_concurrency` decorator.
"""

View File

@ -155,7 +155,7 @@ class Paginator:
@property
def pages(self):
"""class:`list`: Returns the rendered list of pages."""
"""List[:class:`str`]: Returns the rendered list of pages."""
# we have more than just the prefix in our current page
if len(self._current_page) > (0 if self.prefix is None else 1):
self.close_page()
@ -381,7 +381,7 @@ class HelpCommand:
@property
def clean_prefix(self):
"""The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
user = self.context.guild.me if self.context.guild else self.context.bot.user
# this breaks if the prefix mention is not the bot itself but I
# consider this to be an *incredibly* strange use case. I'd rather go
@ -441,6 +441,11 @@ class HelpCommand:
"""Removes mentions from the string to prevent abuse.
This includes ``@everyone``, ``@here``, member mentions and role mentions.
Returns
-------
:class:`str`
The string with mentions removed.
"""
def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
@ -603,6 +608,11 @@ class HelpCommand:
You can override this method to customise the behaviour.
By default this returns the context's channel.
Returns
-------
:class:`.abc.Messageable`
The destination where the help command will be output.
"""
return self.context.channel
@ -911,13 +921,13 @@ class DefaultHelpCommand(HelpCommand):
super().__init__(**options)
def shorten_text(self, text):
"""Shortens text to fit into the :attr:`width`."""
""":class:`str`: Shortens text to fit into the :attr:`width`."""
if len(text) > self.width:
return text[:self.width - 3] + '...'
return text
def get_ending_note(self):
"""Returns help command's ending note. This is mainly useful to override for i18n purposes."""
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
command_name = self.invoked_with
return "Type {0}{1} command for more info on a command.\n" \
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
@ -1122,6 +1132,10 @@ class MinimalHelpCommand(HelpCommand):
Use `{prefix}{command_name} [command]` for more info on a command.
You can also use `{prefix}{command_name} [category]` for more info on a category.
Returns
-------
:class:`str`
The help command opening note.
"""
command_name = self.invoked_with
return "Use `{0}{1} [command]` for more info on a command.\n" \
@ -1134,6 +1148,11 @@ class MinimalHelpCommand(HelpCommand):
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
The default implementation does nothing.
Returns
-------
:class:`str`
The help command ending note.
"""
return None

View File

@ -50,6 +50,9 @@ class flag_value:
def __repr__(self):
return '<flag_value flag={.flag!r}>'.format(self)
class alias_flag_value(flag_value):
pass
def fill_with_flags(*, inverted=False):
def decorator(cls):
cls.VALID_FLAGS = {
@ -98,6 +101,9 @@ class BaseFlags:
def __iter__(self):
for name, value in self.__class__.__dict__.items():
if isinstance(value, alias_flag_value):
continue
if isinstance(value, flag_value):
yield (name, self._has_flag(value.flag))
@ -248,6 +254,14 @@ class PublicUserFlags(BaseFlags):
.. describe:: x != y
Checks if two PublicUserFlags are not equal.
.. describe:: hash(x)
Return the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. versionadded:: 1.4
@ -323,7 +337,15 @@ class PublicUserFlags(BaseFlags):
@flag_value
def verified_bot_developer(self):
""":class:`bool`: Returns ``True`` if the user is a Verified Bot Developer."""
""":class:`bool`: Returns ``True`` if the user is an Early Verified Bot Developer."""
return UserFlags.verified_bot_developer.value
@alias_flag_value
def early_verified_bot_developer(self):
""":class:`bool`: An alias for :attr:`verified_bot_developer`.
.. versionadded:: 1.5
"""
return UserFlags.verified_bot_developer.value
def all(self):
@ -346,9 +368,6 @@ class Intents(BaseFlags):
run your bot. To make use of this, it is passed to the ``intents`` keyword
argument of :class:`Client`.
A default instance of this class has everything enabled except :attr:`presences`
and :attr:`members`.
.. versionadded:: 1.5
.. container:: operations
@ -377,12 +396,7 @@ class Intents(BaseFlags):
__slots__ = ()
def __init__(self, **kwargs):
# Change the default value to everything being enabled
# except presences and members
bits = max(self.VALID_FLAGS.values()).bit_length()
self.value = (1 << bits) - 1
self.presences = False
self.members = False
self.value = self.DEFAULT_VALUE
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key)
@ -404,6 +418,16 @@ class Intents(BaseFlags):
self.value = self.DEFAULT_VALUE
return self
@classmethod
def default(cls):
"""A factory method that creates a :class:`Intents` with everything enabled
except :attr:`presences` and :attr:`members`.
"""
self = cls.all()
self.presences = False
self.members = False
return self
@flag_value
def guilds(self):
""":class:`bool`: Whether guild related events are enabled.
@ -418,6 +442,15 @@ class Intents(BaseFlags):
- :func:`on_guild_channel_create`
- :func:`on_guild_channel_delete`
- :func:`on_guild_channel_pins_update`
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`Client.guilds`
- :class:`Guild` and all its attributes.
- :meth:`Client.get_channel`
- :meth:`Client.get_all_channels`
It is highly advisable to leave this intent enabled for your bot to function.
"""
return 1 << 0
@ -432,9 +465,25 @@ class Intents(BaseFlags):
- :func:`on_member_update` (nickname, roles)
- :func:`on_user_update`
This also corresponds to the following attributes and classes in terms of cache:
- :meth:`Client.get_all_members`
- :meth:`Guild.chunk`
- :meth:`Guild.fetch_members`
- :meth:`Guild.get_member`
- :attr:`Guild.members`
- :attr:`Member.roles`
- :attr:`Member.nick`
- :attr:`Member.premium_since`
- :attr:`User.name`
- :attr:`User.avatar` (:meth:`User.avatar_url` and :meth:`User.avatar_url_as`)
- :attr:`User.discriminator`
For more information go to the :ref:`member intent documentation <need_members_intent>`.
.. note::
Currently, this requires opting in explicitly via the dev portal as well.
Currently, this requires opting in explicitly via the developer portal as well.
Bots in over 100 guilds will need to apply to Discord for verification.
"""
return 1 << 1
@ -447,6 +496,8 @@ class Intents(BaseFlags):
- :func:`on_member_ban`
- :func:`on_member_unban`
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 2
@ -457,6 +508,13 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_guild_emojis_update`
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Emoji`
- :meth:`Client.get_emoji`
- :meth:`Client.emojis`
- :attr:`Guild.emojis`
"""
return 1 << 3
@ -467,6 +525,8 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_guild_integrations_update`
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 4
@ -477,6 +537,8 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_webhooks_update`
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 5
@ -488,6 +550,8 @@ class Intents(BaseFlags):
- :func:`on_invite_create`
- :func:`on_invite_delete`
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 6
@ -498,20 +562,35 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_voice_state_update`
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`VoiceChannel.members`
- :attr:`VoiceChannel.voice_states`
- :attr:`Member.voice`
"""
return 1 << 7
@flag_value
def presences(self):
""":class:`bool`: Whether guild voice state related events are enabled.
""":class:`bool`: Whether guild presence related events are enabled.
This corresponds to the following events:
- :func:`on_member_update` (activities, status)
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`Member.activities`
- :attr:`Member.status`
- :attr:`Member.raw_status`
For more information go to the :ref:`presence intent documentation <need_presence_intent>`.
.. note::
Currently, this requires opting in explicitly via the dev portal as well.
Currently, this requires opting in explicitly via the developer portal as well.
Bots in over 100 guilds will need to apply to Discord for verification.
"""
return 1 << 8
@ -525,11 +604,22 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_message` (both guilds and DMs)
- :func:`on_message_update` (both guilds and DMs)
- :func:`on_message_edit` (both guilds and DMs)
- :func:`on_message_delete` (both guilds and DMs)
- :func:`on_raw_message_delete` (both guilds and DMs)
- :func:`on_raw_message_update` (both guilds and DMs)
- :func:`on_raw_message_edit` (both guilds and DMs)
- :func:`on_private_channel_create`
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Message`
- :attr:`Client.cached_messages`
Note that due to an implicit relationship this also corresponds to the following events:
- :func:`on_reaction_add` (both guilds and DMs)
- :func:`on_reaction_remove` (both guilds and DMs)
- :func:`on_reaction_clear` (both guilds and DMs)
"""
return (1 << 9) | (1 << 12)
@ -542,10 +632,21 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_message` (only for guilds)
- :func:`on_message_update` (only for guilds)
- :func:`on_message_edit` (only for guilds)
- :func:`on_message_delete` (only for guilds)
- :func:`on_raw_message_delete` (only for guilds)
- :func:`on_raw_message_update` (only for guilds)
- :func:`on_raw_message_edit` (only for guilds)
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Message`
- :attr:`Client.cached_messages` (only for guilds)
Note that due to an implicit relationship this also corresponds to the following events:
- :func:`on_reaction_add` (only for guilds)
- :func:`on_reaction_remove` (only for guilds)
- :func:`on_reaction_clear` (only for guilds)
"""
return 1 << 9
@ -558,11 +659,22 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_message` (only for DMs)
- :func:`on_message_update` (only for DMs)
- :func:`on_message_edit` (only for DMs)
- :func:`on_message_delete` (only for DMs)
- :func:`on_raw_message_delete` (only for DMs)
- :func:`on_raw_message_update` (only for DMs)
- :func:`on_raw_message_edit` (only for DMs)
- :func:`on_private_channel_create`
This also corresponds to the following attributes and classes in terms of cache:
- :class:`Message`
- :attr:`Client.cached_messages` (only for DMs)
Note that due to an implicit relationship this also corresponds to the following events:
- :func:`on_reaction_add` (only for DMs)
- :func:`on_reaction_remove` (only for DMs)
- :func:`on_reaction_clear` (only for DMs)
"""
return 1 << 12
@ -580,6 +692,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (both guilds and DMs)
- :func:`on_raw_reaction_remove` (both guilds and DMs)
- :func:`on_raw_reaction_clear` (both guilds and DMs)
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`Message.reactions` (both guild and DM messages)
"""
return (1 << 10) | (1 << 13)
@ -597,6 +713,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (only for guilds)
- :func:`on_raw_reaction_remove` (only for guilds)
- :func:`on_raw_reaction_clear` (only for guilds)
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`Message.reactions` (only for guild messages)
"""
return 1 << 10
@ -614,6 +734,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (only for DMs)
- :func:`on_raw_reaction_remove` (only for DMs)
- :func:`on_raw_reaction_clear` (only for DMs)
This also corresponds to the following attributes and classes in terms of cache:
- :attr:`Message.reactions` (only for DM messages)
"""
return 1 << 13
@ -626,6 +750,8 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_typing` (both guilds and DMs)
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return (1 << 11) | (1 << 14)
@ -638,6 +764,8 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_typing` (only for guilds)
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 11
@ -650,6 +778,8 @@ class Intents(BaseFlags):
This corresponds to the following events:
- :func:`on_typing` (only for DMs)
This does not correspond to any attributes or classes in the library in terms of cache.
"""
return 1 << 14
@ -658,8 +788,8 @@ class MemberCacheFlags(BaseFlags):
"""Controls the library's cache policy when it comes to members.
This allows for finer grained control over what members are cached.
For more information, check :attr:`Client.member_cache_flags`. Note
that the bot's own member is always cached.
Note that the bot's own member is always cached. This class is passed
to the ``member_cache_flags`` parameter in :class:`Client`.
Due to a quirk in how Discord works, in order to ensure proper cleanup
of cache resources it is recommended to have :attr:`Intents.members`
@ -754,6 +884,35 @@ class MemberCacheFlags(BaseFlags):
"""
return 4
@classmethod
def from_intents(cls, intents):
"""A factory method that creates a :class:`MemberCacheFlags` based on
the currently selected :class:`Intents`.
Parameters
------------
intents: :class:`Intents`
The intents to select from.
Returns
---------
:class:`MemberCacheFlags`
The resulting member cache flags.
"""
self = cls.none()
if intents.members:
self.joined = True
if intents.presences:
self.online = True
if intents.voice_states:
self.voice = True
if not self.joined and self.online and self.voice:
self.voice = False
return self
def _verify_intents(self, intents):
if self.online and not intents.presences:
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
@ -765,7 +924,7 @@ class MemberCacheFlags(BaseFlags):
raise ValueError('MemberCacheFlags.joined requires Intents.members')
if not self.joined and self.voice and self.online:
msg = 'MemberCacheFlags.voice and MemberCacheFlags.online require MemberCacheFlags.joined ' \
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
'to properly evict members from the cache.'
raise ValueError(msg)

View File

@ -693,8 +693,8 @@ class DiscordVoiceWebSocket:
Sent only. Tells the client to resume its session.
HELLO
Receive only. Tells you that your websocket connection was acknowledged.
INVALIDATE_SESSION
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
RESUMED
Sent only. Tells you that your RESUME request has succeeded.
CLIENT_CONNECT
Indicates a user has connected to voice.
CLIENT_DISCONNECT
@ -710,7 +710,7 @@ class DiscordVoiceWebSocket:
HEARTBEAT_ACK = 6
RESUME = 7
HELLO = 8
INVALIDATE_SESSION = 9
RESUMED = 9
CLIENT_CONNECT = 12
CLIENT_DISCONNECT = 13
@ -815,9 +815,8 @@ class DiscordVoiceWebSocket:
await self.initial_connection(data)
elif op == self.HEARTBEAT_ACK:
self._keep_alive.ack()
elif op == self.INVALIDATE_SESSION:
log.info('Voice RESUME failed.')
await self.identify()
elif op == self.RESUMED:
log.info('Voice RESUME succeeded.')
elif op == self.SESSION_DESCRIPTION:
self._connection.mode = data['mode']
await self.load_secret_key(data)
@ -833,7 +832,9 @@ class DiscordVoiceWebSocket:
state.endpoint_ip = data['ip']
packet = bytearray(70)
struct.pack_into('>I', packet, 0, state.ssrc)
struct.pack_into('>H', packet, 0, 1) # 1 = Send
struct.pack_into('>H', packet, 2, 70) # 70 = Length
struct.pack_into('>I', packet, 4, state.ssrc)
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
recv = await self.loop.sock_recv(state.socket, 70)
log.debug('received packet in initial_connection: %s', recv)
@ -854,8 +855,6 @@ class DiscordVoiceWebSocket:
await self.select_protocol(state.ip, state.port, mode)
log.info('selected the voice protocol for use (%s)', mode)
await self.client_connect()
@property
def latency(self):
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""

View File

@ -381,7 +381,7 @@ class Guild(Hashable):
@property
def voice_client(self):
"""Optional[:class:`VoiceClient`]: Returns the :class:`VoiceClient` associated with this guild, if any."""
"""Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any."""
return self._state._get_voice_client(self.id)
@property
@ -716,7 +716,14 @@ class Guild(Hashable):
@property
def member_count(self):
""":class:`int`: Returns the true member count regardless of it being loaded fully or not."""
""":class:`int`: Returns the true member count regardless of it being loaded fully or not.
.. warning::
Due to a Discord limitation, in order for this attribute to remain up-to-date and
accurate, it requires :attr:`Intents.members` to be specified.
"""
return self._member_count
@property

View File

@ -497,7 +497,7 @@ class HTTPClient:
def ban(self, user_id, guild_id, delete_message_days=1, reason=None):
r = Route('PUT', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id)
params = {
'delete-message-days': delete_message_days,
'delete_message_days': delete_message_days,
}
if reason:
@ -810,7 +810,7 @@ class HTTPClient:
params = {
'with_counts': int(with_counts)
}
return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id), params=params)
return self.request(Route('GET', '/invites/{invite_id}', invite_id=invite_id), params=params)
def invites_from(self, guild_id):
return self.request(Route('GET', '/guilds/{guild_id}/invites', guild_id=guild_id))
@ -819,7 +819,7 @@ class HTTPClient:
return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id))
def delete_invite(self, invite_id, *, reason=None):
return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id), reason=reason)
return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason)
# Role management

View File

@ -25,13 +25,12 @@ DEALINGS IN THE SOFTWARE.
"""
import datetime
from collections import namedtuple
from .utils import _get_as_snowflake, get, parse_time
from .user import User
from .errors import InvalidArgument
from .enums import try_enum, ExpireBehaviour
class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')):
class IntegrationAccount:
"""Represents an integration account.
.. versionadded:: 1.4
@ -44,7 +43,11 @@ class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')):
The account name.
"""
__slots__ = ()
__slots__ = ('id', 'name')
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
def __repr__(self):
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)

View File

@ -29,9 +29,8 @@ from .utils import parse_time, snowflake_time, _get_as_snowflake
from .object import Object
from .mixins import Hashable
from .enums import ChannelType, VerificationLevel, try_enum
from collections import namedtuple
class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
class PartialInviteChannel:
"""Represents a "partial" invite channel.
This model will be given when the user is not part of the
@ -65,11 +64,19 @@ class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
The partial channel's type.
"""
__slots__ = ()
__slots__ = ('id', 'name', 'type')
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
self.type = kwargs.pop('type')
def __str__(self):
return self.name
def __repr__(self):
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
@property
def mention(self):
""":class:`str`: The string that allows you to mention the channel."""
@ -154,7 +161,7 @@ class PartialInviteGuild:
def icon_url(self):
""":class:`Asset`: Returns the guild's icon asset."""
return self.icon_url_as()
def is_icon_animated(self):
""":class:`bool`: Returns ``True`` if the guild has an animated icon.

View File

@ -200,6 +200,12 @@ class Member(discord.abc.Messageable, _BaseUser):
data['user'] = author._to_minimal_user_json()
return cls(data=data, guild=message.guild, state=message._state)
def _update_from_message(self, data):
self.joined_at = utils.parse_time(data.get('joined_at'))
self.premium_since = utils.parse_time(data.get('premium_since'))
self._update_roles(data)
self.nick = data.get('nick', None)
@classmethod
def _try_upgrade(cls, *, data, guild, state):
# A User object with a 'member' key
@ -315,7 +321,7 @@ class Member(discord.abc.Messageable, _BaseUser):
return try_enum(Status, self._client_status.get('web', 'offline'))
def is_on_mobile(self):
"""A helper function that determines if a member is active on a mobile device."""
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
return 'mobile' in self._client_status
@property
@ -401,6 +407,11 @@ class Member(discord.abc.Messageable, _BaseUser):
-----------
message: :class:`Message`
The message to check if you're mentioned in.
Returns
-------
:class:`bool`
Indicates if the member is mentioned in the message.
"""
if message.guild is None or message.guild.id != self.guild.id:
return False

View File

@ -208,6 +208,37 @@ class Attachment:
data = await self.read(use_cached=use_cached)
return File(io.BytesIO(data), filename=self.filename, spoiler=spoiler)
class MessageReference:
"""Represents a reference to a :class:`Message`.
.. versionadded:: 1.5
Attributes
-----------
message_id: Optional[:class:`int`]
The id of the message referenced.
channel_id: :class:`int`
The channel id of the message referenced.
guild_id: Optional[:class:`int`]
The guild id of the message referenced.
"""
__slots__ = ('message_id', 'channel_id', 'guild_id', '_state')
def __init__(self, state, **kwargs):
self.message_id = utils._get_as_snowflake(kwargs, 'message_id')
self.channel_id = int(kwargs.pop('channel_id'))
self.guild_id = utils._get_as_snowflake(kwargs, 'guild_id')
self._state = state
@property
def cached_message(self):
"""Optional[:class:`Message`]: The cached message, if found in the internal message cache."""
return self._state._get_message(self.message_id)
def __repr__(self):
return '<MessageReference message_id={0.message_id!r} channel_id={0.channel_id!r} guild_id={0.guild_id!r}>'.format(self)
def flatten_handlers(cls):
prefix = len('_handle_')
cls._HANDLERS = {
@ -251,6 +282,13 @@ class Message:
call: Optional[:class:`CallMessage`]
The call that the message refers to. This is only applicable to messages of type
:attr:`MessageType.call`.
reference: Optional[:class:`MessageReference`]
The message that this message references. This is only applicable to messages of
type :attr:`MessageType.pins_add` or crossposted messages created by a
followed channel integration.
.. versionadded:: 1.5
mention_everyone: :class:`bool`
Specifies if the message mentions everyone.
@ -316,7 +354,7 @@ class Message:
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags',
'_cs_system_content', '_cs_guild', '_state', 'reactions',
'_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference',
'application', 'activity')
def __init__(self, *, state, channel, data):
@ -338,6 +376,9 @@ class Message:
self.content = data['content']
self.nonce = data.get('nonce')
ref = data.get('message_reference')
self.reference = MessageReference(state, **ref) if ref is not None else None
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
try:
getattr(self, '_handle_%s' % handler)(data[handler])
@ -476,8 +517,7 @@ class Message:
author = self.author
try:
# Update member reference
if author.joined_at is None:
author.joined_at = utils.parse_time(member.get('joined_at'))
author._update_from_message(member)
except AttributeError:
# It's a user here
# TODO: consider adding to cache here
@ -573,7 +613,7 @@ class Message:
@utils.cached_slot_property('_cs_clean_content')
def clean_content(self):
"""A property that returns the content in a "cleaned up"
""":class:`str`: A property that returns the content in a "cleaned up"
manner. This basically means that mentions are transformed
into the way the client shows it. e.g. ``<#id>`` will transform
into ``#name``.

View File

@ -124,11 +124,11 @@ class PartialEmoji(_EmojiTag):
return hash((self.id, self.name))
def is_custom_emoji(self):
"""Checks if this is a custom non-Unicode emoji."""
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
return self.id is not None
def is_unicode_emoji(self):
"""Checks if this is a Unicode emoji."""
""":class:`bool`: Checks if this is a Unicode emoji."""
return self.id is None
def _as_reaction(self):

View File

@ -24,7 +24,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .flags import BaseFlags, flag_value, fill_with_flags
from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value
__all__ = (
'Permissions',
@ -33,7 +33,7 @@ __all__ = (
# A permission alias works like a regular flag but is marked
# So the PermissionOverwrite knows to work with it
class permission_alias(flag_value):
class permission_alias(alias_flag_value):
pass
def make_permission_alias(alias):
@ -131,14 +131,6 @@ class Permissions(BaseFlags):
__lt__ = is_strict_subset
__gt__ = is_strict_superset
def __iter__(self):
for name, value in self.__class__.__dict__.items():
if isinstance(value, permission_alias):
continue
if isinstance(value, flag_value):
yield (name, self._has_flag(value.flag))
@classmethod
def none(cls):
"""A factory method that creates a :class:`Permissions` with all
@ -495,10 +487,7 @@ class PermissionOverwrite:
self._values[key] = value
def pair(self):
"""Returns the (allow, deny) pair from this overwrite.
The value of these pairs is :class:`Permissions`.
"""
"""Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite."""
allow = Permissions.none()
deny = Permissions.none()
@ -530,6 +519,11 @@ class PermissionOverwrite:
An empty permission overwrite is one that has no overwrites set
to ``True`` or ``False``.
Returns
-------
:class:`bool`
Indicates if the overwrite is empty.
"""
return all(x is None for x in self._values.values())

View File

@ -148,7 +148,7 @@ class Role(Hashable):
self.mentionable = data.get('mentionable', False)
def is_default(self):
"""Checks if the role is the default role."""
""":class:`bool`: Checks if the role is the default role."""
return self.guild.id == self.id
@property

View File

@ -295,14 +295,11 @@ class AutoShardedClient(Client):
elif not isinstance(self.shard_ids, (list, tuple)):
raise ClientException('shard_ids parameter must be a list or a tuple.')
self._connection = AutoShardedConnectionState(dispatch=self.dispatch,
handlers=self._handlers, syncer=self._syncer,
hooks=self._hooks, http=self.http, loop=self.loop, **kwargs)
# instead of a single websocket, we have multiple
# the key is the shard_id
self.__shards = {}
self._connection._get_websocket = self._get_websocket
self._connection._get_client = lambda: self
self.__queue = asyncio.PriorityQueue()
def _get_websocket(self, guild_id=None, *, shard_id=None):
@ -310,6 +307,11 @@ class AutoShardedClient(Client):
shard_id = (guild_id >> 22) % self.shard_count
return self.__shards[shard_id].ws
def _get_state(self, **options):
return AutoShardedConnectionState(dispatch=self.dispatch,
handlers=self._handlers, syncer=self._syncer,
hooks=self._hooks, http=self.http, loop=self.loop, **options)
@property
def latency(self):
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.

View File

@ -32,6 +32,7 @@ import itertools
import logging
import math
import weakref
import warnings
import inspect
import gc
@ -82,6 +83,12 @@ class ChunkRequest:
log = logging.getLogger(__name__)
async def logging_coroutine(coroutine, *, info):
try:
await coroutine
except Exception:
log.exception('Exception occurred during %s', info)
class ConnectionState:
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options):
self.loop = loop
@ -97,7 +104,6 @@ class ConnectionState:
self.hooks = hooks
self.shard_count = None
self._ready_task = None
self._fetch_offline = options.get('fetch_offline_members', True)
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
if self.guild_ready_timeout < 0:
@ -130,21 +136,31 @@ class ConnectionState:
if intents is not None:
if not isinstance(intents, Intents):
raise TypeError('intents parameter must be Intent not %r' % type(intents))
if not intents.members and self._fetch_offline:
raise ValueError('Intents.members has be enabled to fetch offline members.')
else:
intents = Intents()
intents = Intents.default()
try:
chunk_guilds = options['fetch_offline_members']
except KeyError:
chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
else:
msg = 'fetch_offline_members is deprecated, use chunk_guilds_at_startup instead'
warnings.warn(msg, DeprecationWarning, stacklevel=4)
self._chunk_guilds = chunk_guilds
# Ensure these two are set properly
if not intents.members and self._chunk_guilds:
raise ValueError('Intents.members must be enabled to chunk guilds at startup.')
cache_flags = options.get('member_cache_flags', None)
if cache_flags is None:
cache_flags = MemberCacheFlags.all()
cache_flags = MemberCacheFlags.from_intents(intents)
else:
if not isinstance(cache_flags, MemberCacheFlags):
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags))
cache_flags._verify_intents(intents)
cache_flags._verify_intents(intents)
self._member_cache_flags = cache_flags
self._activity = activity
@ -215,6 +231,12 @@ class ConnectionState:
u = self.user
return u.id if u else None
@property
def intents(self):
ret = Intents.none()
ret.value = self._intents.value
return ret
@property
def voice_clients(self):
return list(self._voice_clients.values())
@ -328,7 +350,7 @@ class ConnectionState:
def _guild_needs_chunking(self, guild):
# If presences are enabled then we get back the old guild.large behaviour
return self._fetch_offline and not guild.chunked and not (self._intents.presences and not guild.large)
return self._chunk_guilds and not guild.chunked and not (self._intents.presences and not guild.large)
def _get_guild_channel(self, data):
channel_id = int(data['channel_id'])
@ -941,9 +963,8 @@ class ConnectionState:
if int(data['user_id']) == self.user.id:
voice = self._get_voice_client(guild.id)
if voice is not None:
ch = guild.get_channel(channel_id)
if ch is not None:
voice.channel = ch
coro = voice.on_voice_state_update(data)
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
member, before, after = guild._update_voice_state(data, channel_id)
if member is not None:
@ -971,7 +992,8 @@ class ConnectionState:
vc = self._get_voice_client(key_id)
if vc is not None:
asyncio.ensure_future(vc._create_socket(key_id, data))
coro = vc.on_voice_server_update(data)
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
def parse_typing_start(self, data):
channel, guild = self._get_guild_channel(data)

View File

@ -147,7 +147,7 @@ class BaseUser(_BaseUser):
return str(self.avatar_url_as(static_format="png", size=1024))
def is_avatar_animated(self):
"""Indicates if the user has an animated avatar."""
""":class:`bool`: Indicates if the user has an animated avatar."""
return bool(self.avatar and self.avatar.startswith('a_'))
def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
@ -259,6 +259,11 @@ class BaseUser(_BaseUser):
-----------
message: :class:`Message`
The message to check if you're mentioned in.
Returns
-------
:class:`bool`
Indicates if the user is mentioned in the message.
"""
if message.mention_everyone:
@ -703,6 +708,11 @@ class User(BaseUser, discord.abc.Messageable):
This should be rarely called, as this is done transparently for most
people.
Returns
-------
:class:`.DMChannel`
The channel that was created.
"""
found = self.dm_channel
if found is not None:
@ -748,7 +758,7 @@ class User(BaseUser, discord.abc.Messageable):
return [User(state=state, data=friend) for friend in mutuals]
def is_friend(self):
"""Checks if the user is your friend.
""":class:`bool`: Checks if the user is your friend.
.. note::
@ -760,7 +770,7 @@ class User(BaseUser, discord.abc.Messageable):
return r.type is RelationshipType.friend
def is_blocked(self):
"""Checks if the user is blocked.
""":class:`bool`: Checks if the user is blocked.
.. note::

View File

@ -45,7 +45,7 @@ import logging
import struct
import threading
from . import opus
from . import opus, utils
from .backoff import ExponentialBackoff
from .gateway import *
from .errors import ClientException, ConnectionClosed
@ -59,7 +59,116 @@ except ImportError:
log = logging.getLogger(__name__)
class VoiceClient:
class VoiceProtocol:
"""A class that represents the Discord voice protocol.
This is an abstract class. The library provides a concrete implementation
under :class:`VoiceClient`.
This class allows you to implement a protocol to allow for an external
method of sending voice, such as Lavalink_ or a native library implementation.
These classes are passed to :meth:`abc.Connectable.connect`.
.. _Lavalink: https://github.com/Frederikam/Lavalink
Parameters
------------
client: :class:`Client`
The client (or its subclasses) that started the connection request.
channel: :class:`abc.Connectable`
The voice channel that is being connected to.
"""
def __init__(self, client, channel):
self.client = client
self.channel = channel
async def on_voice_state_update(self, data):
"""|coro|
An abstract method that is called when the client's voice state
has changed. This corresponds to ``VOICE_STATE_UPDATE``.
Parameters
------------
data: :class:`dict`
The raw `voice state payload`__.
.. _voice_state_update_payload: https://discord.com/developers/docs/resources/voice#voice-state-object
__ voice_state_update_payload_
"""
raise NotImplementedError
async def on_voice_server_update(self, data):
"""|coro|
An abstract method that is called when initially connecting to voice.
This corresponds to ``VOICE_SERVER_UPDATE``.
Parameters
------------
data: :class:`dict`
The raw `voice server update payload`__.
.. _voice_server_update_payload: https://discord.com/developers/docs/topics/gateway#voice-server-update-voice-server-update-event-fields
__ voice_server_update_payload_
"""
raise NotImplementedError
async def connect(self, *, timeout, reconnect):
"""|coro|
An abstract method called when the client initiates the connection request.
When a connection is requested initially, the library calls the constructor
under ``__init__`` and then calls :meth:`connect`. If :meth:`connect` fails at
some point then :meth:`disconnect` is called.
Within this method, to start the voice connection flow it is recommended to
use :meth:`Guild.change_voice_state` to start the flow. After which,
:meth:`on_voice_server_update` and :meth:`on_voice_state_update` will be called.
The order that these two are called is unspecified.
Parameters
------------
timeout: :class:`float`
The timeout for the connection.
reconnect: :class:`bool`
Whether reconnection is expected.
"""
raise NotImplementedError
async def disconnect(self, *, force):
"""|coro|
An abstract method called when the client terminates the connection.
See :meth:`cleanup`.
Parameters
------------
force: :class:`bool`
Whether the disconnection was forced.
"""
raise NotImplementedError
def cleanup(self):
"""This method *must* be called to ensure proper clean-up during a disconnect.
It is advisable to call this from within :meth:`disconnect` when you are
completely done with the voice protocol instance.
This method removes it from the internal state cache that keeps track of
currently alive voice clients. Failure to clean-up will cause subsequent
connections to report that it's still connected.
"""
key_id, _ = self.channel._get_voice_client_key()
self.client._connection._remove_voice_client(key_id)
class VoiceClient(VoiceProtocol):
"""Represents a Discord voice connection.
You do not create these, you typically get them from
@ -85,14 +194,13 @@ class VoiceClient:
loop: :class:`asyncio.AbstractEventLoop`
The event loop that the voice client is running on.
"""
def __init__(self, state, timeout, channel):
def __init__(self, client, channel):
if not has_nacl:
raise RuntimeError("PyNaCl library needed in order to use voice")
self.channel = channel
self.main_ws = None
self.timeout = timeout
self.ws = None
super().__init__(client, channel)
state = client._connection
self.token = None
self.socket = None
self.loop = state.loop
self._state = state
@ -100,8 +208,8 @@ class VoiceClient:
self._connected = threading.Event()
self._handshaking = False
self._handshake_check = asyncio.Lock()
self._handshake_complete = asyncio.Event()
self._voice_state_complete = asyncio.Event()
self._voice_server_complete = asyncio.Event()
self.mode = None
self._connections = 0
@ -138,48 +246,28 @@ class VoiceClient:
# connection related
async def start_handshake(self):
log.info('Starting voice handshake...')
async def on_voice_state_update(self, data):
self.session_id = data['session_id']
channel_id = data['channel_id']
guild_id, channel_id = self.channel._get_voice_state_pair()
state = self._state
self.main_ws = ws = state._get_websocket(guild_id)
self._connections += 1
if not self._handshaking:
# If we're done handshaking then we just need to update ourselves
if channel_id is None:
# We're being disconnected so cleanup
await self.disconnect()
else:
guild = self.guild
self.channel = channel_id and guild and guild.get_channel(int(channel_id))
else:
self._voice_state_complete.set()
# request joining
await ws.voice_state(guild_id, channel_id)
async def on_voice_server_update(self, data):
if self._voice_server_complete.is_set():
log.info('Ignoring extraneous voice server update.')
return
try:
await asyncio.wait_for(self._handshake_complete.wait(), timeout=self.timeout)
except asyncio.TimeoutError:
await self.terminate_handshake(remove=True)
raise
log.info('Voice handshake complete. Endpoint found %s (IP: %s)', self.endpoint, self.endpoint_ip)
async def terminate_handshake(self, *, remove=False):
guild_id, channel_id = self.channel._get_voice_state_pair()
self._handshake_complete.clear()
await self.main_ws.voice_state(guild_id, None, self_mute=True)
self._handshaking = False
log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', channel_id, guild_id)
if remove:
log.info('The voice client has been removed for Channel ID %s (Guild ID %s)', channel_id, guild_id)
key_id, _ = self.channel._get_voice_client_key()
self._state._remove_voice_client(key_id)
async def _create_socket(self, server_id, data):
async with self._handshake_check:
if self._handshaking:
log.info("Ignoring voice server update while handshake is in progress")
return
self._handshaking = True
self._connected.clear()
self.session_id = self.main_ws.session_id
self.server_id = server_id
self.token = data.get('token')
self.server_id = int(data['guild_id'])
endpoint = data.get('endpoint')
if endpoint is None or self.token is None:
@ -195,23 +283,77 @@ class VoiceClient:
# This gets set later
self.endpoint_ip = None
if self.socket:
try:
self.socket.close()
except Exception:
pass
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False)
if self._handshake_complete.is_set():
# terminate the websocket and handle the reconnect loop if necessary.
self._handshake_complete.clear()
self._handshaking = False
if not self._handshaking:
# If we're not handshaking then we need to terminate our previous connection in the websocket
await self.ws.close(4000)
return
self._handshake_complete.set()
self._voice_server_complete.set()
async def voice_connect(self):
self._connections += 1
await self.channel.guild.change_voice_state(channel=self.channel)
async def voice_disconnect(self):
log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id)
await self.channel.guild.change_voice_state(channel=None)
async def connect(self, *, reconnect, timeout):
log.info('Connecting to voice...')
self.timeout = timeout
try:
del self.secret_key
except AttributeError:
pass
for i in range(5):
self._voice_state_complete.clear()
self._voice_server_complete.clear()
self._handshaking = True
# This has to be created before we start the flow.
futures = [
self._voice_state_complete.wait(),
self._voice_server_complete.wait(),
]
# Start the connection flow
log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1)
await self.voice_connect()
try:
await utils.sane_wait_for(futures, timeout=timeout)
except asyncio.TimeoutError:
await self.disconnect(force=True)
raise
log.info('Voice handshake complete. Endpoint found %s', self.endpoint)
self._handshaking = False
self._voice_server_complete.clear()
self._voice_state_complete.clear()
try:
self.ws = await DiscordVoiceWebSocket.from_client(self)
self._connected.clear()
while not hasattr(self, 'secret_key'):
await self.ws.poll_event()
self._connected.set()
break
except (ConnectionClosed, asyncio.TimeoutError):
if reconnect:
log.exception('Failed to connect to voice... Retrying...')
await asyncio.sleep(1 + i * 2.0)
await self.voice_disconnect()
continue
else:
raise
if self._runner is None:
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
@property
def latency(self):
@ -234,35 +376,6 @@ class VoiceClient:
ws = self.ws
return float("inf") if not ws else ws.average_latency
async def connect(self, *, reconnect=True, _tries=0, do_handshake=True):
log.info('Connecting to voice...')
try:
del self.secret_key
except AttributeError:
pass
if do_handshake:
await self.start_handshake()
try:
self.ws = await DiscordVoiceWebSocket.from_client(self)
self._handshaking = False
self._connected.clear()
while not hasattr(self, 'secret_key'):
await self.ws.poll_event()
self._connected.set()
except (ConnectionClosed, asyncio.TimeoutError):
if reconnect and _tries < 5:
log.exception('Failed to connect to voice... Retrying...')
await asyncio.sleep(1 + _tries * 2.0)
await self.terminate_handshake()
await self.connect(reconnect=reconnect, _tries=_tries + 1)
else:
raise
if self._runner is None:
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
async def poll_voice_ws(self, reconnect):
backoff = ExponentialBackoff()
while True:
@ -287,9 +400,9 @@ class VoiceClient:
log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry)
self._connected.clear()
await asyncio.sleep(retry)
await self.terminate_handshake()
await self.voice_disconnect()
try:
await self.connect(reconnect=True)
await self.connect(reconnect=True, timeout=self.timeout)
except asyncio.TimeoutError:
# at this point we've retried 5 times... let's continue the loop.
log.warning('Could not connect to voice... Retrying...')
@ -310,8 +423,9 @@ class VoiceClient:
if self.ws:
await self.ws.close()
await self.terminate_handshake(remove=True)
await self.voice_disconnect()
finally:
self.cleanup()
if self.socket:
self.socket.close()
@ -325,8 +439,7 @@ class VoiceClient:
channel: :class:`abc.Snowflake`
The channel to move to. Must be a voice channel.
"""
guild_id, _ = self.channel._get_voice_state_pair()
await self.main_ws.voice_state(guild_id, channel.id)
await self.channel.guild.change_voice_state(channel=channel)
def is_connected(self):
"""Indicates if the voice client is connected to voice."""

View File

@ -29,9 +29,8 @@ from .user import BaseUser
from .activity import create_activity
from .invite import Invite
from .enums import Status, try_enum
from collections import namedtuple
class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
class WidgetChannel:
"""Represents a "partial" widget channel.
.. container:: operations
@ -61,11 +60,20 @@ class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
position: :class:`int`
The channel's position
"""
__slots__ = ()
__slots__ = ('id', 'name', 'position')
def __init__(self, **kwargs):
self.id = kwargs.pop('id')
self.name = kwargs.pop('name')
self.position = kwargs.pop('position')
def __str__(self):
return self.name
def __repr__(self):
return '<WidgetChannel id={0.id} name={0.name!r} position={0.position!r}>'.format(self)
@property
def mention(self):
""":class:`str`: The string that allows you to mention the channel."""

View File

@ -54,6 +54,9 @@ Voice
.. autoclass:: VoiceClient()
:members:
.. autoclass:: VoiceProtocol
:members:
.. autoclass:: AudioSource
:members:
@ -984,6 +987,11 @@ of :class:`enum.Enum`.
.. attribute:: custom
A custom activity type.
.. attribute:: competing
A competing activity type.
.. versionadded:: 1.5
.. class:: HypeSquadHouse
@ -2731,6 +2739,11 @@ Widget
.. autoclass:: Widget()
:members:
MessageReference
~~~~~~~~~~~~~~~~~
.. autoclass:: MessageReference()
:members:
RawMessageDeleteEvent
~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -50,7 +50,7 @@ extlinks = {
# Links used for cross-referencing stuff in other documentation
intersphinx_mapping = {
'py': ('https://docs.python.org/3', None),
'aio': ('https://aiohttp.readthedocs.io/en/stable/', None),
'aio': ('https://docs.aiohttp.org/en/stable/', None),
'req': ('http://docs.python-requests.org/en/latest/', 'requests.inv')
}
@ -318,6 +318,6 @@ texinfo_documents = [
#texinfo_no_detailmenu = False
def setup(app):
app.add_javascript('custom.js')
app.add_js_file('custom.js')
if app.config.language == 'ja':
app.config.intersphinx_mapping['py'] = ('https://docs.python.org/ja/3', None)

View File

@ -344,6 +344,15 @@ Overriding the default provided ``on_message`` forbids any extra commands from r
await bot.process_commands(message)
Alternatively, you can place your ``on_message`` logic into a **listener**. In this setup, you should not
manually call ``bot.process_commands()``. This also allows you to do multiple things asynchronously in response
to a message. Example::
@bot.listen('on_message')
async def whatever_you_want_to_call_it(message):
# do stuff here
# do not process commands here
Why do my arguments require quotes?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -49,6 +49,7 @@ Additional Information
:maxdepth: 2
discord
intents
faq
whats_new
version_guarantees

191
docs/intents.rst Normal file
View File

@ -0,0 +1,191 @@
.. currentmodule:: discord
.. versionadded:: 1.5
.. _intents_primer:
A Primer to Gateway Intents
=============================
In version 1.5 comes the introduction of :class:`Intents`. This is a radical change in how bots are written. An intent basically allows a bot to subscribe into specific buckets of events. The events that correspond to each intent is documented in the individual attribute of the :class:`Intents` documentation.
These intents are passed to the constructor of :class:`Client` or its subclasses (:class:`AutoShardedClient`, :class:`~.AutoShardedBot`, :class:`~.Bot`) with the ``intents`` argument.
If intents are not passed, then the library defaults to every intent being enabled except the privileged intents, currently :attr:`Intents.members` and :attr:`Intents.presences`.
What intents are needed?
--------------------------
The intents that are necessary for your bot can only be dictated by yourself. Each attribute in the :class:`Intents` class documents what :ref:`events <discord-api-events>` it corresponds to and what kind of cache it enables.
For example, if you want a bot that functions without spammy events like presences or typing then we could do the following:
.. code-block:: python3
import discord
intents = discord.Intents.default()
intents.typing = False
intents.presences = False
Note that this doesn't enable :attr:`Intents.members` since it's a privileged intent.
Another example showing a bot that only deals with messages and guild information:
.. code-block:: python3
import discord
intents = discord.Intents(messages=True, guilds=True)
# If you also want reaction events enable the following:
# intents.reactions = True
.. _privileged_intents:
Privileged Intents
---------------------
With the API change requiring bot authors to specify intents, some intents were restricted further and require more manual steps. These intents are called **privileged intents**.
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>`_
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.
.. image:: /images/discord_bot_tab.png
:alt: The bot tab in the application page.
5. Scroll down to the "Privileged Gateway Intents" section and enable the ones you want.
.. image:: /images/discord_privileged_intents.png
:alt: The privileged gateway intents selector.
.. 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.
.. note::
Even if you enable intents through the developer portal, you still have to enable the intents
through code as well.
Do I need privileged intents?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a quick checklist to see if you need specific privileged intents.
.. _need_presence_intent:
Presence Intent
+++++++++++++++++
- Whether you use :attr:`Member.status` at all to track member statuses.
- Whether you use :attr:`Member.activity` or :attr:`Member.activities` to check member's activities.
.. _need_members_intent:
Member Intent
+++++++++++++++
- Whether you track member joins or member leaves, corresponds to :func:`on_member_join` and :func:`on_member_remove` events.
- Whether you want to track member updates such as nickname or role changes.
- Whether you want to track user updates such as usernames, avatars, discriminators, etc.
- Whether you want to request the guild member list through :meth:`Guild.chunk` or :meth:`Guild.fetch_members`.
- Whether you want high accuracy member cache under :attr:`Guild.members`.
.. _intents_member_cache:
Member Cache
-------------
Along with intents, Discord now further restricts the ability to cache members and expects bot authors to cache as little as is necessary. However, to properly maintain a cache the :attr:`Intents.members` intent is required in order to track the members who left and properly evict them.
To aid with member cache where we don't need members to be cached, the library now has a :class:`MemberCacheFlags` flag to control the member cache. The documentation page for the class goes over the specific policies that are possible.
It should be noted that certain things do not need a member cache since Discord will provide full member information if possible. For example:
- :func:`on_message` will have :attr:`Message.author` be a member even if cache is disabled.
- :func:`on_voice_state_update` will have the ``member`` parameter be a member even if cache is disabled.
- :func:`on_reaction_add` will have the ``user`` parameter be a member even if cache is disabled.
- :func:`on_raw_reaction_add` will have :attr:`RawReactionActionEvent.member` be a member even if cache is disabled.
- The reaction removal events do not have the member information. This is a Discord limitation.
Other events that take a :class:`Member` will require the use of the member cache. If absolute accuracy over the member cache is desirable, then it is advisable to have the :attr:`Intents.members` intent enabled.
.. _retrieving_members:
Retrieving Members
--------------------
If cache is disabled or you disable chunking guilds at startup, we might still need a way to load members. The library offers a few ways to do this:
- :meth:`Guild.query_members`
- Used to query members by a prefix matching nickname or username.
- This can also be used to query members by their user ID.
- This uses the gateway and not the HTTP.
- :meth:`Guild.chunk`
- This can be used to fetch the entire member list through the gateway.
- :meth:`Guild.fetch_member`
- Used to fetch a member by ID through the HTTP API.
- :meth:`Guild.fetch_members`
- used to fetch a large number of members through the HTTP API.
It should be noted that the gateway has a strict rate limit of 120 requests per 60 seconds.
Troubleshooting
------------------
Some common issues relating to the mandatory intent change.
Where'd my members go?
~~~~~~~~~~~~~~~~~~~~~~~~
Due to an :ref:`API change <intents_member_cache>` Discord is now forcing developers who want member caching to explicitly opt-in to it. This is a Discord mandated change and there is no way to bypass it. In order to get members back you have to explicitly enable the :ref:`members privileged intent <privileged_intents>` and change the :attr:`Intents.members` attribute to true.
For example:
.. code-block:: python3
import discord
intents = discord.Intents()
intents.members = True
# Somewhere else:
# client = discord.Client(intents=intents)
# or
# from discord.ext import commands
# bot = commands.Bot(command_prefix="!", intents=intents)
Why does ``on_ready`` take so long to fire?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As part of the API change regarding intents, Discord also changed how members are loaded in the beginning. Originally the library could request 75 guilds at once and only request members from guilds that have the :attr:`Guild.large` attribute set to ``True``. With the new intent changes, Discord mandates that we can only send 1 guild per request. This causes a 75x slowdown which is further compounded by the fact that *all* guilds, not just large guilds are being requested.
There are a few solutions to fix this.
The first solution is to request the privileged presences intent along with the privileged members intent and enable both of them. This allows the initial member list to contain online members just like the old gateway. Note that we're still limited to 1 guild per request but the number of guilds we request is significantly reduced.
The second solution is to disable member chunking by setting ``chunk_guilds_at_startup`` to ``False`` when constructing a client. Then, when chunking for a guild is necessary you can use the various techniques to :ref:`retrieve members <retrieving_members>`.
To illustrate the slowdown caused the API change, take a bot who is in 840 guilds and 95 of these guilds are "large" (over 250 members).
Under the original system this would result in 2 requests to fetch the member list (75 guilds, 20 guilds) roughly taking 60 seconds. With :attr:`Intents.members` but not :attr:`Intents.presences` this requires 840 requests, with a rate limit of 120 requests per 60 seconds means that due to waiting for the rate limit it totals to around 7 minutes of waiting for the rate limit to fetch all the members. With both :attr:`Intents.members` and :attr:`Intents.presences` we mostly get the old behaviour so we're only required to request for the 95 guilds that are large, this is slightly less than our rate limit so it's close to the original timing to fetch the member list.
Unfortunately due to this change being required from Discord there is nothing that the library can do to mitigate this.
I don't like this, can I go back?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For now, the old gateway will still work so downgrading to discord.py v1.4 is still possible and will continue to be supported until Discord officially kills the v6 gateway, which is imminent. However it is paramount that for the future of your bot that you upgrade your code to the new way things are done.
To downgrade you can do the following:
.. code-block:: python3
python3 -m pip install -U "discord.py>=1.4,<1.5"
On Windows use ``py -3`` instead of ``python3``.
.. warning::
There is no 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>`_