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 name: Bug Report
about: Report broken or incorrect behaviour about: Report broken or incorrect behaviour
labels: bug
--- ---
### Summary ## Summary
<!-- A summary of your bug report --> <!-- 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. --> <!-- 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 --> <!-- What you expected to happen -->
### Actual Results ## Actual Results
<!-- What actually happened. If there is a traceback, please show the entire thing. --> <!-- 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] --> <!-- 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 shown the entire traceback, if possible.
- [ ] I have removed my token from display, if visible. - [ ] I have removed my token from display, if visible.
### System Information ## System Information
<!-- Run `python -m discord -v` and paste this information below. --> <!-- 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 --> <!-- 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 blank_issues_enabled: false
contact_links: contact_links:
- name: Question about the library - name: Ask a question
about: Support questions are better answered in our Discord server. Issues asking how to implement a feature in your bot will be closed. 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 url: https://discord.gg/r3sSKJJ

View File

@ -1,26 +1,27 @@
--- ---
name: Feature Request name: Feature Request
about: Suggest a feature for this library 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? 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? 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? What is the current solution to the problem, if any?
--> -->
### Summary ## Summary
<!-- A short summary of your feature request. --> <!-- 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? --> <!-- What is this pull request for? Does it fix any issues? -->
### Checklist ## Checklist
<!-- Put an x inside [ ] to check it, like so: [x] --> <!-- 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 .. code:: sh
# Linux/macOS # Linux/macOS
python3 -m pip install -U discord.py[voice] python3 -m pip install -U "discord.py[voice]"
# Windows # Windows
py -3 -m pip install -U discord.py[voice] py -3 -m pip install -U discord.py[voice]

View File

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

View File

@ -37,7 +37,7 @@ from .permissions import PermissionOverwrite, Permissions
from .role import Role from .role import Role
from .invite import Invite from .invite import Invite
from .file import File from .file import File
from .voice_client import VoiceClient from .voice_client import VoiceClient, VoiceProtocol
from . import utils from . import utils
class _Undefined: class _Undefined:
@ -699,6 +699,11 @@ class GuildChannel:
You do not have the proper permissions to create this channel. You do not have the proper permissions to create this channel.
~discord.HTTPException ~discord.HTTPException
Creating the channel failed. Creating the channel failed.
Returns
--------
:class:`.abc.GuildChannel`
The channel that was created.
""" """
raise NotImplementedError raise NotImplementedError
@ -878,7 +883,7 @@ class Messageable(metaclass=abc.ABCMeta):
raise InvalidArgument('file parameter must be File') raise InvalidArgument('file parameter must be File')
try: 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) content=content, tts=tts, embed=embed, nonce=nonce)
finally: finally:
file.close() 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) return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
class Connectable(metaclass=abc.ABCMeta): class Connectable(metaclass=abc.ABCMeta):
"""An ABC that details the common operations on a channel that can """An ABC that details the common operations on a channel that can
connect to a voice server. connect to a voice server.
@ -1068,7 +1072,7 @@ class Connectable(metaclass=abc.ABCMeta):
def _get_voice_state_pair(self): def _get_voice_state_pair(self):
raise NotImplementedError raise NotImplementedError
async def connect(self, *, timeout=60.0, reconnect=True): async def connect(self, *, timeout=60.0, reconnect=True, cls=VoiceClient):
"""|coro| """|coro|
Connects to voice and creates a :class:`VoiceClient` to establish 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 Whether the bot should automatically attempt
a reconnect if a part of the handshake fails a reconnect if a part of the handshake fails
or the gateway goes down. 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 Raises
------- -------
@ -1094,20 +1101,25 @@ class Connectable(metaclass=abc.ABCMeta):
Returns Returns
-------- --------
:class:`~discord.VoiceClient` :class:`~discord.VoiceProtocol`
A voice client that is fully connected to the voice server. 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() key_id, _ = self._get_voice_client_key()
state = self._state state = self._state
if state._get_voice_client(key_id): if state._get_voice_client(key_id):
raise ClientException('Already connected to a voice channel.') 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) state._add_voice_client(key_id, voice)
try: try:
await voice.connect(reconnect=reconnect) await voice.connect(timeout=timeout, reconnect=reconnect)
except asyncio.TimeoutError: except asyncio.TimeoutError:
try: try:
await voice.disconnect(force=True) 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] return [m for m in self.guild.members if self.permissions_for(m).read_messages]
def is_nsfw(self): def is_nsfw(self):
"""Checks if the channel is NSFW.""" """:class:`bool`: Checks if the channel is NSFW."""
return self.nsfw return self.nsfw
def is_news(self): 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 return self._type == ChannelType.news.value
@property @property
@ -757,7 +757,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
return ChannelType.category return ChannelType.category
def is_nsfw(self): def is_nsfw(self):
"""Checks if the category is NSFW.""" """:class:`bool`: Checks if the category is NSFW."""
return self.nsfw return self.nsfw
async def clone(self, *, name=None, reason=None): 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__ permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
def is_nsfw(self): def is_nsfw(self):
"""Checks if the channel is NSFW.""" """:class:`bool`: Checks if the channel is NSFW."""
return self.nsfw return self.nsfw
async def clone(self, *, name=None, reason=None): async def clone(self, *, name=None, reason=None):

View File

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

View File

@ -216,7 +216,13 @@ class Cog(metaclass=CogMeta):
return cleaned return cleaned
def walk_commands(self): 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 from .core import GroupMixin
for command in self.__cog_commands__: for command in self.__cog_commands__:
if command.parent is None: if command.parent is None:

View File

@ -238,7 +238,7 @@ class Context(discord.abc.Messageable):
@property @property
def voice_client(self): 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 g = self.guild
return g.voice_client if g else None return g.voice_client if g else None

View File

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

View File

@ -1180,6 +1180,11 @@ class GroupMixin:
.. versionchanged:: 1.4 .. versionchanged:: 1.4
Duplicates due to aliases are no longer returned 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: for command in self.commands:
yield command yield command
@ -1233,7 +1238,7 @@ class GroupMixin:
Returns Returns
-------- --------
Callable[..., :class:`Command`] 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): def decorator(func):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)
@ -1246,6 +1251,11 @@ class GroupMixin:
def group(self, *args, **kwargs): def group(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.group` and adds it to """A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`. 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): def decorator(func):
kwargs.setdefault('parent', self) kwargs.setdefault('parent', self)

View File

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

View File

@ -155,7 +155,7 @@ class Paginator:
@property @property
def pages(self): 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 # we have more than just the prefix in our current page
if len(self._current_page) > (0 if self.prefix is None else 1): if len(self._current_page) > (0 if self.prefix is None else 1):
self.close_page() self.close_page()
@ -381,7 +381,7 @@ class HelpCommand:
@property @property
def clean_prefix(self): 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 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 # 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 # 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. """Removes mentions from the string to prevent abuse.
This includes ``@everyone``, ``@here``, member mentions and role mentions. This includes ``@everyone``, ``@here``, member mentions and role mentions.
Returns
-------
:class:`str`
The string with mentions removed.
""" """
def replace(obj, *, transforms=self.MENTION_TRANSFORMS): def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
@ -603,6 +608,11 @@ class HelpCommand:
You can override this method to customise the behaviour. You can override this method to customise the behaviour.
By default this returns the context's channel. 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 return self.context.channel
@ -911,13 +921,13 @@ class DefaultHelpCommand(HelpCommand):
super().__init__(**options) super().__init__(**options)
def shorten_text(self, text): 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: if len(text) > self.width:
return text[:self.width - 3] + '...' return text[:self.width - 3] + '...'
return text return text
def get_ending_note(self): 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 command_name = self.invoked_with
return "Type {0}{1} command for more info on a command.\n" \ 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) "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. 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. 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 command_name = self.invoked_with
return "Use `{0}{1} [command]` for more info on a command.\n" \ 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. """Return the help command's ending note. This is mainly useful to override for i18n purposes.
The default implementation does nothing. The default implementation does nothing.
Returns
-------
:class:`str`
The help command ending note.
""" """
return None return None

View File

@ -50,6 +50,9 @@ class flag_value:
def __repr__(self): def __repr__(self):
return '<flag_value flag={.flag!r}>'.format(self) return '<flag_value flag={.flag!r}>'.format(self)
class alias_flag_value(flag_value):
pass
def fill_with_flags(*, inverted=False): def fill_with_flags(*, inverted=False):
def decorator(cls): def decorator(cls):
cls.VALID_FLAGS = { cls.VALID_FLAGS = {
@ -98,6 +101,9 @@ class BaseFlags:
def __iter__(self): def __iter__(self):
for name, value in self.__class__.__dict__.items(): for name, value in self.__class__.__dict__.items():
if isinstance(value, alias_flag_value):
continue
if isinstance(value, flag_value): if isinstance(value, flag_value):
yield (name, self._has_flag(value.flag)) yield (name, self._has_flag(value.flag))
@ -248,6 +254,14 @@ class PublicUserFlags(BaseFlags):
.. describe:: x != y .. describe:: x != y
Checks if two PublicUserFlags are not equal. 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 .. versionadded:: 1.4
@ -323,7 +337,15 @@ class PublicUserFlags(BaseFlags):
@flag_value @flag_value
def verified_bot_developer(self): 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 return UserFlags.verified_bot_developer.value
def all(self): 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 run your bot. To make use of this, it is passed to the ``intents`` keyword
argument of :class:`Client`. argument of :class:`Client`.
A default instance of this class has everything enabled except :attr:`presences`
and :attr:`members`.
.. versionadded:: 1.5 .. versionadded:: 1.5
.. container:: operations .. container:: operations
@ -377,12 +396,7 @@ class Intents(BaseFlags):
__slots__ = () __slots__ = ()
def __init__(self, **kwargs): def __init__(self, **kwargs):
# Change the default value to everything being enabled self.value = self.DEFAULT_VALUE
# except presences and members
bits = max(self.VALID_FLAGS.values()).bit_length()
self.value = (1 << bits) - 1
self.presences = False
self.members = False
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in self.VALID_FLAGS: if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key) raise TypeError('%r is not a valid flag name.' % key)
@ -404,6 +418,16 @@ class Intents(BaseFlags):
self.value = self.DEFAULT_VALUE self.value = self.DEFAULT_VALUE
return self 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 @flag_value
def guilds(self): def guilds(self):
""":class:`bool`: Whether guild related events are enabled. """:class:`bool`: Whether guild related events are enabled.
@ -418,6 +442,15 @@ class Intents(BaseFlags):
- :func:`on_guild_channel_create` - :func:`on_guild_channel_create`
- :func:`on_guild_channel_delete` - :func:`on_guild_channel_delete`
- :func:`on_guild_channel_pins_update` - :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 return 1 << 0
@ -432,9 +465,25 @@ class Intents(BaseFlags):
- :func:`on_member_update` (nickname, roles) - :func:`on_member_update` (nickname, roles)
- :func:`on_user_update` - :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:: .. 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. Bots in over 100 guilds will need to apply to Discord for verification.
""" """
return 1 << 1 return 1 << 1
@ -447,6 +496,8 @@ class Intents(BaseFlags):
- :func:`on_member_ban` - :func:`on_member_ban`
- :func:`on_member_unban` - :func:`on_member_unban`
This does not correspond to any attributes or classes in the library in terms of cache.
""" """
return 1 << 2 return 1 << 2
@ -457,6 +508,13 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_guild_emojis_update` - :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 return 1 << 3
@ -467,6 +525,8 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_guild_integrations_update` - :func:`on_guild_integrations_update`
This does not correspond to any attributes or classes in the library in terms of cache.
""" """
return 1 << 4 return 1 << 4
@ -477,6 +537,8 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_webhooks_update` - :func:`on_webhooks_update`
This does not correspond to any attributes or classes in the library in terms of cache.
""" """
return 1 << 5 return 1 << 5
@ -488,6 +550,8 @@ class Intents(BaseFlags):
- :func:`on_invite_create` - :func:`on_invite_create`
- :func:`on_invite_delete` - :func:`on_invite_delete`
This does not correspond to any attributes or classes in the library in terms of cache.
""" """
return 1 << 6 return 1 << 6
@ -498,20 +562,35 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_voice_state_update` - :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 return 1 << 7
@flag_value @flag_value
def presences(self): 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: This corresponds to the following events:
- :func:`on_member_update` (activities, status) - :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:: .. 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. Bots in over 100 guilds will need to apply to Discord for verification.
""" """
return 1 << 8 return 1 << 8
@ -525,11 +604,22 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_message` (both guilds and DMs) - :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_message_delete` (both guilds and DMs)
- :func:`on_raw_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` - :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) return (1 << 9) | (1 << 12)
@ -542,10 +632,21 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_message` (only for guilds) - :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_message_delete` (only for guilds)
- :func:`on_raw_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 return 1 << 9
@ -558,11 +659,22 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_message` (only for DMs) - :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_message_delete` (only for DMs)
- :func:`on_raw_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` - :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 return 1 << 12
@ -580,6 +692,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (both guilds and DMs) - :func:`on_raw_reaction_add` (both guilds and DMs)
- :func:`on_raw_reaction_remove` (both guilds and DMs) - :func:`on_raw_reaction_remove` (both guilds and DMs)
- :func:`on_raw_reaction_clear` (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) return (1 << 10) | (1 << 13)
@ -597,6 +713,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (only for guilds) - :func:`on_raw_reaction_add` (only for guilds)
- :func:`on_raw_reaction_remove` (only for guilds) - :func:`on_raw_reaction_remove` (only for guilds)
- :func:`on_raw_reaction_clear` (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 return 1 << 10
@ -614,6 +734,10 @@ class Intents(BaseFlags):
- :func:`on_raw_reaction_add` (only for DMs) - :func:`on_raw_reaction_add` (only for DMs)
- :func:`on_raw_reaction_remove` (only for DMs) - :func:`on_raw_reaction_remove` (only for DMs)
- :func:`on_raw_reaction_clear` (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 return 1 << 13
@ -626,6 +750,8 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_typing` (both guilds and DMs) - :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) return (1 << 11) | (1 << 14)
@ -638,6 +764,8 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_typing` (only for guilds) - :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 return 1 << 11
@ -650,6 +778,8 @@ class Intents(BaseFlags):
This corresponds to the following events: This corresponds to the following events:
- :func:`on_typing` (only for DMs) - :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 return 1 << 14
@ -658,8 +788,8 @@ class MemberCacheFlags(BaseFlags):
"""Controls the library's cache policy when it comes to members. """Controls the library's cache policy when it comes to members.
This allows for finer grained control over what members are cached. This allows for finer grained control over what members are cached.
For more information, check :attr:`Client.member_cache_flags`. Note Note that the bot's own member is always cached. This class is passed
that the bot's own member is always cached. to the ``member_cache_flags`` parameter in :class:`Client`.
Due to a quirk in how Discord works, in order to ensure proper cleanup 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` of cache resources it is recommended to have :attr:`Intents.members`
@ -754,6 +884,35 @@ class MemberCacheFlags(BaseFlags):
""" """
return 4 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): def _verify_intents(self, intents):
if self.online and not intents.presences: if self.online and not intents.presences:
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled') raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
@ -765,7 +924,7 @@ class MemberCacheFlags(BaseFlags):
raise ValueError('MemberCacheFlags.joined requires Intents.members') raise ValueError('MemberCacheFlags.joined requires Intents.members')
if not self.joined and self.voice and self.online: 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.' 'to properly evict members from the cache.'
raise ValueError(msg) raise ValueError(msg)

View File

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

View File

@ -381,7 +381,7 @@ class Guild(Hashable):
@property @property
def voice_client(self): 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) return self._state._get_voice_client(self.id)
@property @property
@ -716,7 +716,14 @@ class Guild(Hashable):
@property @property
def member_count(self): 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 return self._member_count
@property @property

View File

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

View File

@ -25,13 +25,12 @@ DEALINGS IN THE SOFTWARE.
""" """
import datetime import datetime
from collections import namedtuple
from .utils import _get_as_snowflake, get, parse_time from .utils import _get_as_snowflake, get, parse_time
from .user import User from .user import User
from .errors import InvalidArgument from .errors import InvalidArgument
from .enums import try_enum, ExpireBehaviour from .enums import try_enum, ExpireBehaviour
class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')): class IntegrationAccount:
"""Represents an integration account. """Represents an integration account.
.. versionadded:: 1.4 .. versionadded:: 1.4
@ -44,7 +43,11 @@ class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')):
The account 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): def __repr__(self):
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(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 .object import Object
from .mixins import Hashable from .mixins import Hashable
from .enums import ChannelType, VerificationLevel, try_enum 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. """Represents a "partial" invite channel.
This model will be given when the user is not part of the 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. 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): def __str__(self):
return self.name return self.name
def __repr__(self):
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
@property @property
def mention(self): def mention(self):
""":class:`str`: The string that allows you to mention the channel.""" """:class:`str`: The string that allows you to mention the channel."""
@ -154,7 +161,7 @@ class PartialInviteGuild:
def icon_url(self): def icon_url(self):
""":class:`Asset`: Returns the guild's icon asset.""" """:class:`Asset`: Returns the guild's icon asset."""
return self.icon_url_as() return self.icon_url_as()
def is_icon_animated(self): def is_icon_animated(self):
""":class:`bool`: Returns ``True`` if the guild has an animated icon. """: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() data['user'] = author._to_minimal_user_json()
return cls(data=data, guild=message.guild, state=message._state) 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 @classmethod
def _try_upgrade(cls, *, data, guild, state): def _try_upgrade(cls, *, data, guild, state):
# A User object with a 'member' key # 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')) return try_enum(Status, self._client_status.get('web', 'offline'))
def is_on_mobile(self): 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 return 'mobile' in self._client_status
@property @property
@ -401,6 +407,11 @@ class Member(discord.abc.Messageable, _BaseUser):
----------- -----------
message: :class:`Message` message: :class:`Message`
The message to check if you're mentioned in. 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: if message.guild is None or message.guild.id != self.guild.id:
return False return False

View File

@ -208,6 +208,37 @@ class Attachment:
data = await self.read(use_cached=use_cached) data = await self.read(use_cached=use_cached)
return File(io.BytesIO(data), filename=self.filename, spoiler=spoiler) 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): def flatten_handlers(cls):
prefix = len('_handle_') prefix = len('_handle_')
cls._HANDLERS = { cls._HANDLERS = {
@ -251,6 +282,13 @@ class Message:
call: Optional[:class:`CallMessage`] call: Optional[:class:`CallMessage`]
The call that the message refers to. This is only applicable to messages of type The call that the message refers to. This is only applicable to messages of type
:attr:`MessageType.call`. :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` mention_everyone: :class:`bool`
Specifies if the message mentions everyone. Specifies if the message mentions everyone.
@ -316,7 +354,7 @@ class Message:
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments', '_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned', '_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags', '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') 'application', 'activity')
def __init__(self, *, state, channel, data): def __init__(self, *, state, channel, data):
@ -338,6 +376,9 @@ class Message:
self.content = data['content'] self.content = data['content']
self.nonce = data.get('nonce') 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'): for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
try: try:
getattr(self, '_handle_%s' % handler)(data[handler]) getattr(self, '_handle_%s' % handler)(data[handler])
@ -476,8 +517,7 @@ class Message:
author = self.author author = self.author
try: try:
# Update member reference # Update member reference
if author.joined_at is None: author._update_from_message(member)
author.joined_at = utils.parse_time(member.get('joined_at'))
except AttributeError: except AttributeError:
# It's a user here # It's a user here
# TODO: consider adding to cache here # TODO: consider adding to cache here
@ -573,7 +613,7 @@ class Message:
@utils.cached_slot_property('_cs_clean_content') @utils.cached_slot_property('_cs_clean_content')
def clean_content(self): 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 manner. This basically means that mentions are transformed
into the way the client shows it. e.g. ``<#id>`` will transform into the way the client shows it. e.g. ``<#id>`` will transform
into ``#name``. into ``#name``.

View File

@ -124,11 +124,11 @@ class PartialEmoji(_EmojiTag):
return hash((self.id, self.name)) return hash((self.id, self.name))
def is_custom_emoji(self): 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 return self.id is not None
def is_unicode_emoji(self): 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 return self.id is None
def _as_reaction(self): 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. 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__ = ( __all__ = (
'Permissions', 'Permissions',
@ -33,7 +33,7 @@ __all__ = (
# A permission alias works like a regular flag but is marked # A permission alias works like a regular flag but is marked
# So the PermissionOverwrite knows to work with it # So the PermissionOverwrite knows to work with it
class permission_alias(flag_value): class permission_alias(alias_flag_value):
pass pass
def make_permission_alias(alias): def make_permission_alias(alias):
@ -131,14 +131,6 @@ class Permissions(BaseFlags):
__lt__ = is_strict_subset __lt__ = is_strict_subset
__gt__ = is_strict_superset __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 @classmethod
def none(cls): def none(cls):
"""A factory method that creates a :class:`Permissions` with all """A factory method that creates a :class:`Permissions` with all
@ -495,10 +487,7 @@ class PermissionOverwrite:
self._values[key] = value self._values[key] = value
def pair(self): def pair(self):
"""Returns the (allow, deny) pair from this overwrite. """Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite."""
The value of these pairs is :class:`Permissions`.
"""
allow = Permissions.none() allow = Permissions.none()
deny = Permissions.none() deny = Permissions.none()
@ -530,6 +519,11 @@ class PermissionOverwrite:
An empty permission overwrite is one that has no overwrites set An empty permission overwrite is one that has no overwrites set
to ``True`` or ``False``. to ``True`` or ``False``.
Returns
-------
:class:`bool`
Indicates if the overwrite is empty.
""" """
return all(x is None for x in self._values.values()) 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) self.mentionable = data.get('mentionable', False)
def is_default(self): 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 return self.guild.id == self.id
@property @property

View File

@ -295,14 +295,11 @@ class AutoShardedClient(Client):
elif not isinstance(self.shard_ids, (list, tuple)): elif not isinstance(self.shard_ids, (list, tuple)):
raise ClientException('shard_ids parameter must be a list or a 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 # instead of a single websocket, we have multiple
# the key is the shard_id # the key is the shard_id
self.__shards = {} self.__shards = {}
self._connection._get_websocket = self._get_websocket self._connection._get_websocket = self._get_websocket
self._connection._get_client = lambda: self
self.__queue = asyncio.PriorityQueue() self.__queue = asyncio.PriorityQueue()
def _get_websocket(self, guild_id=None, *, shard_id=None): 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 shard_id = (guild_id >> 22) % self.shard_count
return self.__shards[shard_id].ws 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 @property
def latency(self): def latency(self):
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.

View File

@ -32,6 +32,7 @@ import itertools
import logging import logging
import math import math
import weakref import weakref
import warnings
import inspect import inspect
import gc import gc
@ -82,6 +83,12 @@ class ChunkRequest:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
async def logging_coroutine(coroutine, *, info):
try:
await coroutine
except Exception:
log.exception('Exception occurred during %s', info)
class ConnectionState: class ConnectionState:
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options): def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options):
self.loop = loop self.loop = loop
@ -97,7 +104,6 @@ class ConnectionState:
self.hooks = hooks self.hooks = hooks
self.shard_count = None self.shard_count = None
self._ready_task = None self._ready_task = None
self._fetch_offline = options.get('fetch_offline_members', True)
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0) self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0) self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
if self.guild_ready_timeout < 0: if self.guild_ready_timeout < 0:
@ -130,21 +136,31 @@ class ConnectionState:
if intents is not None: if intents is not None:
if not isinstance(intents, Intents): if not isinstance(intents, Intents):
raise TypeError('intents parameter must be Intent not %r' % type(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: 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) cache_flags = options.get('member_cache_flags', None)
if cache_flags is None: if cache_flags is None:
cache_flags = MemberCacheFlags.all() cache_flags = MemberCacheFlags.from_intents(intents)
else: else:
if not isinstance(cache_flags, MemberCacheFlags): if not isinstance(cache_flags, MemberCacheFlags):
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags)) 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._member_cache_flags = cache_flags
self._activity = activity self._activity = activity
@ -215,6 +231,12 @@ class ConnectionState:
u = self.user u = self.user
return u.id if u else None return u.id if u else None
@property
def intents(self):
ret = Intents.none()
ret.value = self._intents.value
return ret
@property @property
def voice_clients(self): def voice_clients(self):
return list(self._voice_clients.values()) return list(self._voice_clients.values())
@ -328,7 +350,7 @@ class ConnectionState:
def _guild_needs_chunking(self, guild): def _guild_needs_chunking(self, guild):
# If presences are enabled then we get back the old guild.large behaviour # 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): def _get_guild_channel(self, data):
channel_id = int(data['channel_id']) channel_id = int(data['channel_id'])
@ -941,9 +963,8 @@ class ConnectionState:
if int(data['user_id']) == self.user.id: if int(data['user_id']) == self.user.id:
voice = self._get_voice_client(guild.id) voice = self._get_voice_client(guild.id)
if voice is not None: if voice is not None:
ch = guild.get_channel(channel_id) coro = voice.on_voice_state_update(data)
if ch is not None: asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
voice.channel = ch
member, before, after = guild._update_voice_state(data, channel_id) member, before, after = guild._update_voice_state(data, channel_id)
if member is not None: if member is not None:
@ -971,7 +992,8 @@ class ConnectionState:
vc = self._get_voice_client(key_id) vc = self._get_voice_client(key_id)
if vc is not None: 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): def parse_typing_start(self, data):
channel, guild = self._get_guild_channel(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)) return str(self.avatar_url_as(static_format="png", size=1024))
def is_avatar_animated(self): 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_')) return bool(self.avatar and self.avatar.startswith('a_'))
def avatar_url_as(self, *, format=None, static_format='webp', size=1024): def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
@ -259,6 +259,11 @@ class BaseUser(_BaseUser):
----------- -----------
message: :class:`Message` message: :class:`Message`
The message to check if you're mentioned in. 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: 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 This should be rarely called, as this is done transparently for most
people. people.
Returns
-------
:class:`.DMChannel`
The channel that was created.
""" """
found = self.dm_channel found = self.dm_channel
if found is not None: 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] return [User(state=state, data=friend) for friend in mutuals]
def is_friend(self): def is_friend(self):
"""Checks if the user is your friend. """:class:`bool`: Checks if the user is your friend.
.. note:: .. note::
@ -760,7 +770,7 @@ class User(BaseUser, discord.abc.Messageable):
return r.type is RelationshipType.friend return r.type is RelationshipType.friend
def is_blocked(self): def is_blocked(self):
"""Checks if the user is blocked. """:class:`bool`: Checks if the user is blocked.
.. note:: .. note::

View File

@ -45,7 +45,7 @@ import logging
import struct import struct
import threading import threading
from . import opus from . import opus, utils
from .backoff import ExponentialBackoff from .backoff import ExponentialBackoff
from .gateway import * from .gateway import *
from .errors import ClientException, ConnectionClosed from .errors import ClientException, ConnectionClosed
@ -59,7 +59,116 @@ except ImportError:
log = logging.getLogger(__name__) 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. """Represents a Discord voice connection.
You do not create these, you typically get them from You do not create these, you typically get them from
@ -85,14 +194,13 @@ class VoiceClient:
loop: :class:`asyncio.AbstractEventLoop` loop: :class:`asyncio.AbstractEventLoop`
The event loop that the voice client is running on. 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: if not has_nacl:
raise RuntimeError("PyNaCl library needed in order to use voice") raise RuntimeError("PyNaCl library needed in order to use voice")
self.channel = channel super().__init__(client, channel)
self.main_ws = None state = client._connection
self.timeout = timeout self.token = None
self.ws = None
self.socket = None self.socket = None
self.loop = state.loop self.loop = state.loop
self._state = state self._state = state
@ -100,8 +208,8 @@ class VoiceClient:
self._connected = threading.Event() self._connected = threading.Event()
self._handshaking = False self._handshaking = False
self._handshake_check = asyncio.Lock() self._voice_state_complete = asyncio.Event()
self._handshake_complete = asyncio.Event() self._voice_server_complete = asyncio.Event()
self.mode = None self.mode = None
self._connections = 0 self._connections = 0
@ -138,48 +246,28 @@ class VoiceClient:
# connection related # connection related
async def start_handshake(self): async def on_voice_state_update(self, data):
log.info('Starting voice handshake...') self.session_id = data['session_id']
channel_id = data['channel_id']
guild_id, channel_id = self.channel._get_voice_state_pair() if not self._handshaking:
state = self._state # If we're done handshaking then we just need to update ourselves
self.main_ws = ws = state._get_websocket(guild_id) if channel_id is None:
self._connections += 1 # 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 async def on_voice_server_update(self, data):
await ws.voice_state(guild_id, channel_id) 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.token = data.get('token')
self.server_id = int(data['guild_id'])
endpoint = data.get('endpoint') endpoint = data.get('endpoint')
if endpoint is None or self.token is None: if endpoint is None or self.token is None:
@ -195,23 +283,77 @@ class VoiceClient:
# This gets set later # This gets set later
self.endpoint_ip = None 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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setblocking(False) self.socket.setblocking(False)
if self._handshake_complete.is_set(): if not self._handshaking:
# terminate the websocket and handle the reconnect loop if necessary. # If we're not handshaking then we need to terminate our previous connection in the websocket
self._handshake_complete.clear()
self._handshaking = False
await self.ws.close(4000) await self.ws.close(4000)
return 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 @property
def latency(self): def latency(self):
@ -234,35 +376,6 @@ class VoiceClient:
ws = self.ws ws = self.ws
return float("inf") if not ws else ws.average_latency 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): async def poll_voice_ws(self, reconnect):
backoff = ExponentialBackoff() backoff = ExponentialBackoff()
while True: while True:
@ -287,9 +400,9 @@ class VoiceClient:
log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry)
self._connected.clear() self._connected.clear()
await asyncio.sleep(retry) await asyncio.sleep(retry)
await self.terminate_handshake() await self.voice_disconnect()
try: try:
await self.connect(reconnect=True) await self.connect(reconnect=True, timeout=self.timeout)
except asyncio.TimeoutError: except asyncio.TimeoutError:
# at this point we've retried 5 times... let's continue the loop. # at this point we've retried 5 times... let's continue the loop.
log.warning('Could not connect to voice... Retrying...') log.warning('Could not connect to voice... Retrying...')
@ -310,8 +423,9 @@ class VoiceClient:
if self.ws: if self.ws:
await self.ws.close() await self.ws.close()
await self.terminate_handshake(remove=True) await self.voice_disconnect()
finally: finally:
self.cleanup()
if self.socket: if self.socket:
self.socket.close() self.socket.close()
@ -325,8 +439,7 @@ class VoiceClient:
channel: :class:`abc.Snowflake` channel: :class:`abc.Snowflake`
The channel to move to. Must be a voice channel. The channel to move to. Must be a voice channel.
""" """
guild_id, _ = self.channel._get_voice_state_pair() await self.channel.guild.change_voice_state(channel=channel)
await self.main_ws.voice_state(guild_id, channel.id)
def is_connected(self): def is_connected(self):
"""Indicates if the voice client is connected to voice.""" """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 .activity import create_activity
from .invite import Invite from .invite import Invite
from .enums import Status, try_enum from .enums import Status, try_enum
from collections import namedtuple
class WidgetChannel(namedtuple('WidgetChannel', 'id name position')): class WidgetChannel:
"""Represents a "partial" widget channel. """Represents a "partial" widget channel.
.. container:: operations .. container:: operations
@ -61,11 +60,20 @@ class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
position: :class:`int` position: :class:`int`
The channel's position 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): def __str__(self):
return self.name return self.name
def __repr__(self):
return '<WidgetChannel id={0.id} name={0.name!r} position={0.position!r}>'.format(self)
@property @property
def mention(self): def mention(self):
""":class:`str`: The string that allows you to mention the channel.""" """:class:`str`: The string that allows you to mention the channel."""

View File

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

View File

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

View File

@ -344,6 +344,15 @@ Overriding the default provided ``on_message`` forbids any extra commands from r
await bot.process_commands(message) 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? 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 :maxdepth: 2
discord discord
intents
faq faq
whats_new whats_new
version_guarantees 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>`_