fixes
This commit is contained in:
commit
64be57b192
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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 -->
|
||||||
|
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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
|
||||||
|
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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. -->
|
||||||
|
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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] -->
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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 *
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
203
discord/flags.py
203
discord/flags.py
@ -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)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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``.
|
||||||
|
@ -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):
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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,16 +136,26 @@ 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))
|
||||||
@ -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)
|
||||||
|
@ -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::
|
||||||
|
|
||||||
|
@ -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.')
|
||||||
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
|
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."""
|
||||||
|
@ -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."""
|
||||||
|
13
docs/api.rst
13
docs/api.rst
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
BIN
docs/images/discord_bot_tab.png
Normal file
BIN
docs/images/discord_bot_tab.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/images/discord_privileged_intents.png
Normal file
BIN
docs/images/discord_privileged_intents.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
@ -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
191
docs/intents.rst
Normal 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>`_
|
Loading…
x
Reference in New Issue
Block a user