fixes
This commit is contained in:
@ -33,7 +33,7 @@ from .guild import Guild
|
||||
from .flags import *
|
||||
from .relationship import Relationship
|
||||
from .member import Member, VoiceState
|
||||
from .message import Message, Attachment
|
||||
from .message import Message, MessageReference, Attachment
|
||||
from .asset import Asset
|
||||
from .errors import *
|
||||
from .calls import CallMessage, GroupCall
|
||||
@ -54,7 +54,7 @@ from .mentions import AllowedMentions
|
||||
from .shard import AutoShardedClient, ShardInfo
|
||||
from .player import *
|
||||
from .webhook import *
|
||||
from .voice_client import VoiceClient
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff
|
||||
from .raw_models import *
|
||||
from .team import *
|
||||
|
@ -37,7 +37,7 @@ from .permissions import PermissionOverwrite, Permissions
|
||||
from .role import Role
|
||||
from .invite import Invite
|
||||
from .file import File
|
||||
from .voice_client import VoiceClient
|
||||
from .voice_client import VoiceClient, VoiceProtocol
|
||||
from . import utils
|
||||
|
||||
class _Undefined:
|
||||
@ -699,6 +699,11 @@ class GuildChannel:
|
||||
You do not have the proper permissions to create this channel.
|
||||
~discord.HTTPException
|
||||
Creating the channel failed.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`.abc.GuildChannel`
|
||||
The channel that was created.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -878,7 +883,7 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
raise InvalidArgument('file parameter must be File')
|
||||
|
||||
try:
|
||||
data = await state.http.send_files(channel.id, files=[file],
|
||||
data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions,
|
||||
content=content, tts=tts, embed=embed, nonce=nonce)
|
||||
finally:
|
||||
file.close()
|
||||
@ -1049,7 +1054,6 @@ class Messageable(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
|
||||
|
||||
|
||||
class Connectable(metaclass=abc.ABCMeta):
|
||||
"""An ABC that details the common operations on a channel that can
|
||||
connect to a voice server.
|
||||
@ -1068,7 +1072,7 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
def _get_voice_state_pair(self):
|
||||
raise NotImplementedError
|
||||
|
||||
async def connect(self, *, timeout=60.0, reconnect=True):
|
||||
async def connect(self, *, timeout=60.0, reconnect=True, cls=VoiceClient):
|
||||
"""|coro|
|
||||
|
||||
Connects to voice and creates a :class:`VoiceClient` to establish
|
||||
@ -1082,6 +1086,9 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
Whether the bot should automatically attempt
|
||||
a reconnect if a part of the handshake fails
|
||||
or the gateway goes down.
|
||||
cls: Type[:class:`VoiceProtocol`]
|
||||
A type that subclasses :class:`~discord.VoiceProtocol` to connect with.
|
||||
Defaults to :class:`~discord.VoiceClient`.
|
||||
|
||||
Raises
|
||||
-------
|
||||
@ -1094,20 +1101,25 @@ class Connectable(metaclass=abc.ABCMeta):
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`~discord.VoiceClient`
|
||||
:class:`~discord.VoiceProtocol`
|
||||
A voice client that is fully connected to the voice server.
|
||||
"""
|
||||
|
||||
if not issubclass(cls, VoiceProtocol):
|
||||
raise TypeError('Type must meet VoiceProtocol abstract base class.')
|
||||
|
||||
key_id, _ = self._get_voice_client_key()
|
||||
state = self._state
|
||||
|
||||
if state._get_voice_client(key_id):
|
||||
raise ClientException('Already connected to a voice channel.')
|
||||
|
||||
voice = VoiceClient(state=state, timeout=timeout, channel=self)
|
||||
client = state._get_client()
|
||||
voice = cls(client, self)
|
||||
state._add_voice_client(key_id, voice)
|
||||
|
||||
try:
|
||||
await voice.connect(reconnect=reconnect)
|
||||
await voice.connect(timeout=timeout, reconnect=reconnect)
|
||||
except asyncio.TimeoutError:
|
||||
try:
|
||||
await voice.disconnect(force=True)
|
||||
|
@ -158,11 +158,11 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
||||
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
||||
|
||||
def is_nsfw(self):
|
||||
"""Checks if the channel is NSFW."""
|
||||
""":class:`bool`: Checks if the channel is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
def is_news(self):
|
||||
"""Checks if the channel is a news channel."""
|
||||
""":class:`bool`: Checks if the channel is a news channel."""
|
||||
return self._type == ChannelType.news.value
|
||||
|
||||
@property
|
||||
@ -757,7 +757,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
||||
return ChannelType.category
|
||||
|
||||
def is_nsfw(self):
|
||||
"""Checks if the category is NSFW."""
|
||||
""":class:`bool`: Checks if the category is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
@ -933,7 +933,7 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
permissions_for.__doc__ = discord.abc.GuildChannel.permissions_for.__doc__
|
||||
|
||||
def is_nsfw(self):
|
||||
"""Checks if the channel is NSFW."""
|
||||
""":class:`bool`: Checks if the channel is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
async def clone(self, *, name=None, reason=None):
|
||||
|
@ -25,7 +25,6 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
@ -144,17 +143,24 @@ class Client:
|
||||
intents: :class:`Intents`
|
||||
The intents that you want to enable for the session. This is a way of
|
||||
disabling and enabling certain gateway events from triggering and being sent.
|
||||
If not given, defaults to a regularly constructed :class:`Intents` class.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
member_cache_flags: :class:`MemberCacheFlags`
|
||||
Allows for finer control over how the library caches members.
|
||||
If not given, defaults to cache as much as possible with the
|
||||
currently selected intents.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
fetch_offline_members: :class:`bool`
|
||||
Indicates if :func:`.on_ready` should be delayed to fetch all offline
|
||||
members from the guilds the client belongs to. If this is ``False``\, then
|
||||
no offline members are received and :meth:`request_offline_members`
|
||||
must be used to fetch the offline members of the guild.
|
||||
A deprecated alias of ``chunk_guilds_at_startup``.
|
||||
chunk_guilds_at_startup: :class:`bool`
|
||||
Indicates if :func:`.on_ready` should be delayed to chunk all guilds
|
||||
at start-up if necessary. This operation is incredibly slow for large
|
||||
amounts of guilds. The default is ``True`` if :attr:`Intents.members`
|
||||
is ``True``.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
status: Optional[:class:`.Status`]
|
||||
A status to start your presence with upon logging on to Discord.
|
||||
activity: Optional[:class:`.BaseActivity`]
|
||||
@ -241,13 +247,12 @@ class Client:
|
||||
'before_identify': self._call_before_identify_hook
|
||||
}
|
||||
|
||||
self._connection = ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
|
||||
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
|
||||
|
||||
self._connection = self._get_state(**options)
|
||||
self._connection.shard_count = self.shard_count
|
||||
self._closed = False
|
||||
self._ready = asyncio.Event()
|
||||
self._connection._get_websocket = self._get_websocket
|
||||
self._connection._get_client = lambda: self
|
||||
|
||||
if VoiceClient.warn_nacl:
|
||||
VoiceClient.warn_nacl = False
|
||||
@ -258,6 +263,10 @@ class Client:
|
||||
def _get_websocket(self, guild_id=None, *, shard_id=None):
|
||||
return self.ws
|
||||
|
||||
def _get_state(self, **options):
|
||||
return ConnectionState(dispatch=self.dispatch, handlers=self._handlers,
|
||||
hooks=self._hooks, syncer=self._syncer, http=self.http, loop=self.loop, **options)
|
||||
|
||||
async def _syncer(self, guilds):
|
||||
await self.ws.request_sync(guilds)
|
||||
|
||||
@ -309,11 +318,14 @@ class Client:
|
||||
|
||||
@property
|
||||
def voice_clients(self):
|
||||
"""List[:class:`.VoiceClient`]: Represents a list of voice connections."""
|
||||
"""List[:class:`.VoiceProtocol`]: Represents a list of voice connections.
|
||||
|
||||
These are usually :class:`.VoiceClient` instances.
|
||||
"""
|
||||
return self._connection.voice_clients
|
||||
|
||||
def is_ready(self):
|
||||
"""Specifies if the client's internal cache is ready for use."""
|
||||
""":class:`bool`: Specifies if the client's internal cache is ready for use."""
|
||||
return self._ready.is_set()
|
||||
|
||||
async def _run_event(self, coro, event_name, *args, **kwargs):
|
||||
@ -701,7 +713,7 @@ class Client:
|
||||
# properties
|
||||
|
||||
def is_closed(self):
|
||||
"""Indicates if the websocket connection is closed."""
|
||||
""":class:`bool`: Indicates if the websocket connection is closed."""
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
@ -737,6 +749,14 @@ class Client:
|
||||
else:
|
||||
raise TypeError('allowed_mentions must be AllowedMentions not {0.__class__!r}'.format(value))
|
||||
|
||||
@property
|
||||
def intents(self):
|
||||
""":class:`Intents`: The intents configured for this connection.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
return self._connection.intents
|
||||
|
||||
# helpers/getters
|
||||
|
||||
@property
|
||||
@ -818,6 +838,11 @@ class Client:
|
||||
Just because you receive a :class:`.abc.GuildChannel` does not mean that
|
||||
you can communicate in said channel. :meth:`.abc.GuildChannel.permissions_for` should
|
||||
be used for that.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`.abc.GuildChannel`
|
||||
A channel the client can 'access'.
|
||||
"""
|
||||
|
||||
for guild in self.guilds:
|
||||
@ -832,6 +857,11 @@ class Client:
|
||||
for guild in client.guilds:
|
||||
for member in guild.members:
|
||||
yield member
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`.Member`
|
||||
A member the client can see.
|
||||
"""
|
||||
for guild in self.guilds:
|
||||
for member in guild.members:
|
||||
|
@ -216,7 +216,13 @@ class Cog(metaclass=CogMeta):
|
||||
return cleaned
|
||||
|
||||
def walk_commands(self):
|
||||
"""An iterator that recursively walks through this cog's commands and subcommands."""
|
||||
"""An iterator that recursively walks through this cog's commands and subcommands.
|
||||
|
||||
Yields
|
||||
------
|
||||
Union[:class:`.Command`, :class:`.Group`]
|
||||
A command or group from the cog.
|
||||
"""
|
||||
from .core import GroupMixin
|
||||
for command in self.__cog_commands__:
|
||||
if command.parent is None:
|
||||
|
@ -238,7 +238,7 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
r"""Optional[:class:`.VoiceClient`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
|
||||
r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable."""
|
||||
g = self.guild
|
||||
return g.voice_client if g else None
|
||||
|
||||
|
@ -362,6 +362,7 @@ class CategoryChannelConverter(IDConverter):
|
||||
|
||||
class ColourConverter(Converter):
|
||||
"""Converts to a :class:`~discord.Colour`.
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Add an alias named ColorConverter
|
||||
|
||||
|
@ -1180,6 +1180,11 @@ class GroupMixin:
|
||||
|
||||
.. versionchanged:: 1.4
|
||||
Duplicates due to aliases are no longer returned
|
||||
|
||||
Yields
|
||||
------
|
||||
Union[:class:`.Command`, :class:`.Group`]
|
||||
A command or group from the internal list of commands.
|
||||
"""
|
||||
for command in self.commands:
|
||||
yield command
|
||||
@ -1233,7 +1238,7 @@ class GroupMixin:
|
||||
Returns
|
||||
--------
|
||||
Callable[..., :class:`Command`]
|
||||
A decorator that converts the provided method into a Command, adds it to the bot, then returns it
|
||||
A decorator that converts the provided method into a Command, adds it to the bot, then returns it.
|
||||
"""
|
||||
def decorator(func):
|
||||
kwargs.setdefault('parent', self)
|
||||
@ -1246,6 +1251,11 @@ class GroupMixin:
|
||||
def group(self, *args, **kwargs):
|
||||
"""A shortcut decorator that invokes :func:`.group` and adds it to
|
||||
the internal command list via :meth:`~.GroupMixin.add_command`.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Callable[..., :class:`Group`]
|
||||
A decorator that converts the provided method into a Group, adds it to the bot, then returns it.
|
||||
"""
|
||||
def decorator(func):
|
||||
kwargs.setdefault('parent', self)
|
||||
|
@ -273,7 +273,7 @@ class ChannelNotReadable(BadArgument):
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`Channel`
|
||||
argument: :class:`.abc.GuildChannel`
|
||||
The channel supplied by the caller that was not readable
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
@ -403,7 +403,7 @@ class CommandInvokeError(CommandError):
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original
|
||||
original: :exc:`Exception`
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
@ -438,7 +438,7 @@ class MaxConcurrencyReached(CommandError):
|
||||
------------
|
||||
number: :class:`int`
|
||||
The maximum number of concurrent invokers allowed.
|
||||
per: :class:`BucketType`
|
||||
per: :class:`.BucketType`
|
||||
The bucket type passed to the :func:`.max_concurrency` decorator.
|
||||
"""
|
||||
|
||||
|
@ -155,7 +155,7 @@ class Paginator:
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
"""class:`list`: Returns the rendered list of pages."""
|
||||
"""List[:class:`str`]: Returns the rendered list of pages."""
|
||||
# we have more than just the prefix in our current page
|
||||
if len(self._current_page) > (0 if self.prefix is None else 1):
|
||||
self.close_page()
|
||||
@ -381,7 +381,7 @@ class HelpCommand:
|
||||
|
||||
@property
|
||||
def clean_prefix(self):
|
||||
"""The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
|
||||
""":class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
|
||||
user = self.context.guild.me if self.context.guild else self.context.bot.user
|
||||
# this breaks if the prefix mention is not the bot itself but I
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
@ -441,6 +441,11 @@ class HelpCommand:
|
||||
"""Removes mentions from the string to prevent abuse.
|
||||
|
||||
This includes ``@everyone``, ``@here``, member mentions and role mentions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`str`
|
||||
The string with mentions removed.
|
||||
"""
|
||||
|
||||
def replace(obj, *, transforms=self.MENTION_TRANSFORMS):
|
||||
@ -603,6 +608,11 @@ class HelpCommand:
|
||||
You can override this method to customise the behaviour.
|
||||
|
||||
By default this returns the context's channel.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`.abc.Messageable`
|
||||
The destination where the help command will be output.
|
||||
"""
|
||||
return self.context.channel
|
||||
|
||||
@ -911,13 +921,13 @@ class DefaultHelpCommand(HelpCommand):
|
||||
super().__init__(**options)
|
||||
|
||||
def shorten_text(self, text):
|
||||
"""Shortens text to fit into the :attr:`width`."""
|
||||
""":class:`str`: Shortens text to fit into the :attr:`width`."""
|
||||
if len(text) > self.width:
|
||||
return text[:self.width - 3] + '...'
|
||||
return text
|
||||
|
||||
def get_ending_note(self):
|
||||
"""Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
|
||||
command_name = self.invoked_with
|
||||
return "Type {0}{1} command for more info on a command.\n" \
|
||||
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
|
||||
@ -1122,6 +1132,10 @@ class MinimalHelpCommand(HelpCommand):
|
||||
Use `{prefix}{command_name} [command]` for more info on a command.
|
||||
You can also use `{prefix}{command_name} [category]` for more info on a category.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`str`
|
||||
The help command opening note.
|
||||
"""
|
||||
command_name = self.invoked_with
|
||||
return "Use `{0}{1} [command]` for more info on a command.\n" \
|
||||
@ -1134,6 +1148,11 @@ class MinimalHelpCommand(HelpCommand):
|
||||
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`str`
|
||||
The help command ending note.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
203
discord/flags.py
203
discord/flags.py
@ -50,6 +50,9 @@ class flag_value:
|
||||
def __repr__(self):
|
||||
return '<flag_value flag={.flag!r}>'.format(self)
|
||||
|
||||
class alias_flag_value(flag_value):
|
||||
pass
|
||||
|
||||
def fill_with_flags(*, inverted=False):
|
||||
def decorator(cls):
|
||||
cls.VALID_FLAGS = {
|
||||
@ -98,6 +101,9 @@ class BaseFlags:
|
||||
|
||||
def __iter__(self):
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, alias_flag_value):
|
||||
continue
|
||||
|
||||
if isinstance(value, flag_value):
|
||||
yield (name, self._has_flag(value.flag))
|
||||
|
||||
@ -248,6 +254,14 @@ class PublicUserFlags(BaseFlags):
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if two PublicUserFlags are not equal.
|
||||
.. describe:: hash(x)
|
||||
|
||||
Return the flag's hash.
|
||||
.. describe:: iter(x)
|
||||
|
||||
Returns an iterator of ``(name, value)`` pairs. This allows it
|
||||
to be, for example, constructed as a dict or a list of pairs.
|
||||
Note that aliases are not shown.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
@ -323,7 +337,15 @@ class PublicUserFlags(BaseFlags):
|
||||
|
||||
@flag_value
|
||||
def verified_bot_developer(self):
|
||||
""":class:`bool`: Returns ``True`` if the user is a Verified Bot Developer."""
|
||||
""":class:`bool`: Returns ``True`` if the user is an Early Verified Bot Developer."""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
@alias_flag_value
|
||||
def early_verified_bot_developer(self):
|
||||
""":class:`bool`: An alias for :attr:`verified_bot_developer`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
"""
|
||||
return UserFlags.verified_bot_developer.value
|
||||
|
||||
def all(self):
|
||||
@ -346,9 +368,6 @@ class Intents(BaseFlags):
|
||||
run your bot. To make use of this, it is passed to the ``intents`` keyword
|
||||
argument of :class:`Client`.
|
||||
|
||||
A default instance of this class has everything enabled except :attr:`presences`
|
||||
and :attr:`members`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
.. container:: operations
|
||||
@ -377,12 +396,7 @@ class Intents(BaseFlags):
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Change the default value to everything being enabled
|
||||
# except presences and members
|
||||
bits = max(self.VALID_FLAGS.values()).bit_length()
|
||||
self.value = (1 << bits) - 1
|
||||
self.presences = False
|
||||
self.members = False
|
||||
self.value = self.DEFAULT_VALUE
|
||||
for key, value in kwargs.items():
|
||||
if key not in self.VALID_FLAGS:
|
||||
raise TypeError('%r is not a valid flag name.' % key)
|
||||
@ -404,6 +418,16 @@ class Intents(BaseFlags):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||
except :attr:`presences` and :attr:`members`.
|
||||
"""
|
||||
self = cls.all()
|
||||
self.presences = False
|
||||
self.members = False
|
||||
return self
|
||||
|
||||
@flag_value
|
||||
def guilds(self):
|
||||
""":class:`bool`: Whether guild related events are enabled.
|
||||
@ -418,6 +442,15 @@ class Intents(BaseFlags):
|
||||
- :func:`on_guild_channel_create`
|
||||
- :func:`on_guild_channel_delete`
|
||||
- :func:`on_guild_channel_pins_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Client.guilds`
|
||||
- :class:`Guild` and all its attributes.
|
||||
- :meth:`Client.get_channel`
|
||||
- :meth:`Client.get_all_channels`
|
||||
|
||||
It is highly advisable to leave this intent enabled for your bot to function.
|
||||
"""
|
||||
return 1 << 0
|
||||
|
||||
@ -432,9 +465,25 @@ class Intents(BaseFlags):
|
||||
- :func:`on_member_update` (nickname, roles)
|
||||
- :func:`on_user_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :meth:`Client.get_all_members`
|
||||
- :meth:`Guild.chunk`
|
||||
- :meth:`Guild.fetch_members`
|
||||
- :meth:`Guild.get_member`
|
||||
- :attr:`Guild.members`
|
||||
- :attr:`Member.roles`
|
||||
- :attr:`Member.nick`
|
||||
- :attr:`Member.premium_since`
|
||||
- :attr:`User.name`
|
||||
- :attr:`User.avatar` (:meth:`User.avatar_url` and :meth:`User.avatar_url_as`)
|
||||
- :attr:`User.discriminator`
|
||||
|
||||
For more information go to the :ref:`member intent documentation <need_members_intent>`.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, this requires opting in explicitly via the dev portal as well.
|
||||
Currently, this requires opting in explicitly via the developer portal as well.
|
||||
Bots in over 100 guilds will need to apply to Discord for verification.
|
||||
"""
|
||||
return 1 << 1
|
||||
@ -447,6 +496,8 @@ class Intents(BaseFlags):
|
||||
|
||||
- :func:`on_member_ban`
|
||||
- :func:`on_member_unban`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 2
|
||||
|
||||
@ -457,6 +508,13 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_emojis_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Emoji`
|
||||
- :meth:`Client.get_emoji`
|
||||
- :meth:`Client.emojis`
|
||||
- :attr:`Guild.emojis`
|
||||
"""
|
||||
return 1 << 3
|
||||
|
||||
@ -467,6 +525,8 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_guild_integrations_update`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 4
|
||||
|
||||
@ -477,6 +537,8 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_webhooks_update`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 5
|
||||
|
||||
@ -488,6 +550,8 @@ class Intents(BaseFlags):
|
||||
|
||||
- :func:`on_invite_create`
|
||||
- :func:`on_invite_delete`
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 6
|
||||
|
||||
@ -498,20 +562,35 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_voice_state_update`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`VoiceChannel.members`
|
||||
- :attr:`VoiceChannel.voice_states`
|
||||
- :attr:`Member.voice`
|
||||
"""
|
||||
return 1 << 7
|
||||
|
||||
@flag_value
|
||||
def presences(self):
|
||||
""":class:`bool`: Whether guild voice state related events are enabled.
|
||||
|
||||
""":class:`bool`: Whether guild presence related events are enabled.
|
||||
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_member_update` (activities, status)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Member.activities`
|
||||
- :attr:`Member.status`
|
||||
- :attr:`Member.raw_status`
|
||||
|
||||
For more information go to the :ref:`presence intent documentation <need_presence_intent>`.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, this requires opting in explicitly via the dev portal as well.
|
||||
Currently, this requires opting in explicitly via the developer portal as well.
|
||||
Bots in over 100 guilds will need to apply to Discord for verification.
|
||||
"""
|
||||
return 1 << 8
|
||||
@ -525,11 +604,22 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (both guilds and DMs)
|
||||
- :func:`on_message_update` (both guilds and DMs)
|
||||
- :func:`on_message_edit` (both guilds and DMs)
|
||||
- :func:`on_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_delete` (both guilds and DMs)
|
||||
- :func:`on_raw_message_update` (both guilds and DMs)
|
||||
- :func:`on_raw_message_edit` (both guilds and DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages`
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (both guilds and DMs)
|
||||
- :func:`on_reaction_remove` (both guilds and DMs)
|
||||
- :func:`on_reaction_clear` (both guilds and DMs)
|
||||
"""
|
||||
return (1 << 9) | (1 << 12)
|
||||
|
||||
@ -542,10 +632,21 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (only for guilds)
|
||||
- :func:`on_message_update` (only for guilds)
|
||||
- :func:`on_message_edit` (only for guilds)
|
||||
- :func:`on_message_delete` (only for guilds)
|
||||
- :func:`on_raw_message_delete` (only for guilds)
|
||||
- :func:`on_raw_message_update` (only for guilds)
|
||||
- :func:`on_raw_message_edit` (only for guilds)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages` (only for guilds)
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for guilds)
|
||||
- :func:`on_reaction_remove` (only for guilds)
|
||||
- :func:`on_reaction_clear` (only for guilds)
|
||||
"""
|
||||
return 1 << 9
|
||||
|
||||
@ -558,11 +659,22 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_message` (only for DMs)
|
||||
- :func:`on_message_update` (only for DMs)
|
||||
- :func:`on_message_edit` (only for DMs)
|
||||
- :func:`on_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_delete` (only for DMs)
|
||||
- :func:`on_raw_message_update` (only for DMs)
|
||||
- :func:`on_raw_message_edit` (only for DMs)
|
||||
- :func:`on_private_channel_create`
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :class:`Message`
|
||||
- :attr:`Client.cached_messages` (only for DMs)
|
||||
|
||||
Note that due to an implicit relationship this also corresponds to the following events:
|
||||
|
||||
- :func:`on_reaction_add` (only for DMs)
|
||||
- :func:`on_reaction_remove` (only for DMs)
|
||||
- :func:`on_reaction_clear` (only for DMs)
|
||||
"""
|
||||
return 1 << 12
|
||||
|
||||
@ -580,6 +692,10 @@ class Intents(BaseFlags):
|
||||
- :func:`on_raw_reaction_add` (both guilds and DMs)
|
||||
- :func:`on_raw_reaction_remove` (both guilds and DMs)
|
||||
- :func:`on_raw_reaction_clear` (both guilds and DMs)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (both guild and DM messages)
|
||||
"""
|
||||
return (1 << 10) | (1 << 13)
|
||||
|
||||
@ -597,6 +713,10 @@ class Intents(BaseFlags):
|
||||
- :func:`on_raw_reaction_add` (only for guilds)
|
||||
- :func:`on_raw_reaction_remove` (only for guilds)
|
||||
- :func:`on_raw_reaction_clear` (only for guilds)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (only for guild messages)
|
||||
"""
|
||||
return 1 << 10
|
||||
|
||||
@ -614,6 +734,10 @@ class Intents(BaseFlags):
|
||||
- :func:`on_raw_reaction_add` (only for DMs)
|
||||
- :func:`on_raw_reaction_remove` (only for DMs)
|
||||
- :func:`on_raw_reaction_clear` (only for DMs)
|
||||
|
||||
This also corresponds to the following attributes and classes in terms of cache:
|
||||
|
||||
- :attr:`Message.reactions` (only for DM messages)
|
||||
"""
|
||||
return 1 << 13
|
||||
|
||||
@ -626,6 +750,8 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (both guilds and DMs)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return (1 << 11) | (1 << 14)
|
||||
|
||||
@ -638,6 +764,8 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (only for guilds)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 11
|
||||
|
||||
@ -650,6 +778,8 @@ class Intents(BaseFlags):
|
||||
This corresponds to the following events:
|
||||
|
||||
- :func:`on_typing` (only for DMs)
|
||||
|
||||
This does not correspond to any attributes or classes in the library in terms of cache.
|
||||
"""
|
||||
return 1 << 14
|
||||
|
||||
@ -658,8 +788,8 @@ class MemberCacheFlags(BaseFlags):
|
||||
"""Controls the library's cache policy when it comes to members.
|
||||
|
||||
This allows for finer grained control over what members are cached.
|
||||
For more information, check :attr:`Client.member_cache_flags`. Note
|
||||
that the bot's own member is always cached.
|
||||
Note that the bot's own member is always cached. This class is passed
|
||||
to the ``member_cache_flags`` parameter in :class:`Client`.
|
||||
|
||||
Due to a quirk in how Discord works, in order to ensure proper cleanup
|
||||
of cache resources it is recommended to have :attr:`Intents.members`
|
||||
@ -754,6 +884,35 @@ class MemberCacheFlags(BaseFlags):
|
||||
"""
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def from_intents(cls, intents):
|
||||
"""A factory method that creates a :class:`MemberCacheFlags` based on
|
||||
the currently selected :class:`Intents`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
intents: :class:`Intents`
|
||||
The intents to select from.
|
||||
|
||||
Returns
|
||||
---------
|
||||
:class:`MemberCacheFlags`
|
||||
The resulting member cache flags.
|
||||
"""
|
||||
|
||||
self = cls.none()
|
||||
if intents.members:
|
||||
self.joined = True
|
||||
if intents.presences:
|
||||
self.online = True
|
||||
if intents.voice_states:
|
||||
self.voice = True
|
||||
|
||||
if not self.joined and self.online and self.voice:
|
||||
self.voice = False
|
||||
|
||||
return self
|
||||
|
||||
def _verify_intents(self, intents):
|
||||
if self.online and not intents.presences:
|
||||
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
|
||||
@ -765,7 +924,7 @@ class MemberCacheFlags(BaseFlags):
|
||||
raise ValueError('MemberCacheFlags.joined requires Intents.members')
|
||||
|
||||
if not self.joined and self.voice and self.online:
|
||||
msg = 'MemberCacheFlags.voice and MemberCacheFlags.online require MemberCacheFlags.joined ' \
|
||||
msg = 'Setting both MemberCacheFlags.voice and MemberCacheFlags.online requires MemberCacheFlags.joined ' \
|
||||
'to properly evict members from the cache.'
|
||||
raise ValueError(msg)
|
||||
|
||||
|
@ -693,8 +693,8 @@ class DiscordVoiceWebSocket:
|
||||
Sent only. Tells the client to resume its session.
|
||||
HELLO
|
||||
Receive only. Tells you that your websocket connection was acknowledged.
|
||||
INVALIDATE_SESSION
|
||||
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
|
||||
RESUMED
|
||||
Sent only. Tells you that your RESUME request has succeeded.
|
||||
CLIENT_CONNECT
|
||||
Indicates a user has connected to voice.
|
||||
CLIENT_DISCONNECT
|
||||
@ -710,7 +710,7 @@ class DiscordVoiceWebSocket:
|
||||
HEARTBEAT_ACK = 6
|
||||
RESUME = 7
|
||||
HELLO = 8
|
||||
INVALIDATE_SESSION = 9
|
||||
RESUMED = 9
|
||||
CLIENT_CONNECT = 12
|
||||
CLIENT_DISCONNECT = 13
|
||||
|
||||
@ -815,9 +815,8 @@ class DiscordVoiceWebSocket:
|
||||
await self.initial_connection(data)
|
||||
elif op == self.HEARTBEAT_ACK:
|
||||
self._keep_alive.ack()
|
||||
elif op == self.INVALIDATE_SESSION:
|
||||
log.info('Voice RESUME failed.')
|
||||
await self.identify()
|
||||
elif op == self.RESUMED:
|
||||
log.info('Voice RESUME succeeded.')
|
||||
elif op == self.SESSION_DESCRIPTION:
|
||||
self._connection.mode = data['mode']
|
||||
await self.load_secret_key(data)
|
||||
@ -833,7 +832,9 @@ class DiscordVoiceWebSocket:
|
||||
state.endpoint_ip = data['ip']
|
||||
|
||||
packet = bytearray(70)
|
||||
struct.pack_into('>I', packet, 0, state.ssrc)
|
||||
struct.pack_into('>H', packet, 0, 1) # 1 = Send
|
||||
struct.pack_into('>H', packet, 2, 70) # 70 = Length
|
||||
struct.pack_into('>I', packet, 4, state.ssrc)
|
||||
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))
|
||||
recv = await self.loop.sock_recv(state.socket, 70)
|
||||
log.debug('received packet in initial_connection: %s', recv)
|
||||
@ -854,8 +855,6 @@ class DiscordVoiceWebSocket:
|
||||
await self.select_protocol(state.ip, state.port, mode)
|
||||
log.info('selected the voice protocol for use (%s)', mode)
|
||||
|
||||
await self.client_connect()
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""
|
||||
|
@ -381,7 +381,7 @@ class Guild(Hashable):
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
"""Optional[:class:`VoiceClient`]: Returns the :class:`VoiceClient` associated with this guild, if any."""
|
||||
"""Optional[:class:`VoiceProtocol`]: Returns the :class:`VoiceProtocol` associated with this guild, if any."""
|
||||
return self._state._get_voice_client(self.id)
|
||||
|
||||
@property
|
||||
@ -716,7 +716,14 @@ class Guild(Hashable):
|
||||
|
||||
@property
|
||||
def member_count(self):
|
||||
""":class:`int`: Returns the true member count regardless of it being loaded fully or not."""
|
||||
""":class:`int`: Returns the true member count regardless of it being loaded fully or not.
|
||||
|
||||
.. warning::
|
||||
|
||||
Due to a Discord limitation, in order for this attribute to remain up-to-date and
|
||||
accurate, it requires :attr:`Intents.members` to be specified.
|
||||
|
||||
"""
|
||||
return self._member_count
|
||||
|
||||
@property
|
||||
|
@ -497,7 +497,7 @@ class HTTPClient:
|
||||
def ban(self, user_id, guild_id, delete_message_days=1, reason=None):
|
||||
r = Route('PUT', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id)
|
||||
params = {
|
||||
'delete-message-days': delete_message_days,
|
||||
'delete_message_days': delete_message_days,
|
||||
}
|
||||
|
||||
if reason:
|
||||
@ -810,7 +810,7 @@ class HTTPClient:
|
||||
params = {
|
||||
'with_counts': int(with_counts)
|
||||
}
|
||||
return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id), params=params)
|
||||
return self.request(Route('GET', '/invites/{invite_id}', invite_id=invite_id), params=params)
|
||||
|
||||
def invites_from(self, guild_id):
|
||||
return self.request(Route('GET', '/guilds/{guild_id}/invites', guild_id=guild_id))
|
||||
@ -819,7 +819,7 @@ class HTTPClient:
|
||||
return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id))
|
||||
|
||||
def delete_invite(self, invite_id, *, reason=None):
|
||||
return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id), reason=reason)
|
||||
return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason)
|
||||
|
||||
# Role management
|
||||
|
||||
|
@ -25,13 +25,12 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from collections import namedtuple
|
||||
from .utils import _get_as_snowflake, get, parse_time
|
||||
from .user import User
|
||||
from .errors import InvalidArgument
|
||||
from .enums import try_enum, ExpireBehaviour
|
||||
|
||||
class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')):
|
||||
class IntegrationAccount:
|
||||
"""Represents an integration account.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
@ -44,7 +43,11 @@ class IntegrationAccount(namedtuple('IntegrationAccount', 'id name')):
|
||||
The account name.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ('id', 'name')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
|
||||
def __repr__(self):
|
||||
return '<IntegrationAccount id={0.id} name={0.name!r}>'.format(self)
|
||||
|
@ -29,9 +29,8 @@ from .utils import parse_time, snowflake_time, _get_as_snowflake
|
||||
from .object import Object
|
||||
from .mixins import Hashable
|
||||
from .enums import ChannelType, VerificationLevel, try_enum
|
||||
from collections import namedtuple
|
||||
|
||||
class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
|
||||
class PartialInviteChannel:
|
||||
"""Represents a "partial" invite channel.
|
||||
|
||||
This model will be given when the user is not part of the
|
||||
@ -65,11 +64,19 @@ class PartialInviteChannel(namedtuple('PartialInviteChannel', 'id name type')):
|
||||
The partial channel's type.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ('id', 'name', 'type')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
self.type = kwargs.pop('type')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<PartialInviteChannel id={0.id} name={0.name} type={0.type!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
@ -154,7 +161,7 @@ class PartialInviteGuild:
|
||||
def icon_url(self):
|
||||
""":class:`Asset`: Returns the guild's icon asset."""
|
||||
return self.icon_url_as()
|
||||
|
||||
|
||||
def is_icon_animated(self):
|
||||
""":class:`bool`: Returns ``True`` if the guild has an animated icon.
|
||||
|
||||
|
@ -200,6 +200,12 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
data['user'] = author._to_minimal_user_json()
|
||||
return cls(data=data, guild=message.guild, state=message._state)
|
||||
|
||||
def _update_from_message(self, data):
|
||||
self.joined_at = utils.parse_time(data.get('joined_at'))
|
||||
self.premium_since = utils.parse_time(data.get('premium_since'))
|
||||
self._update_roles(data)
|
||||
self.nick = data.get('nick', None)
|
||||
|
||||
@classmethod
|
||||
def _try_upgrade(cls, *, data, guild, state):
|
||||
# A User object with a 'member' key
|
||||
@ -315,7 +321,7 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
return try_enum(Status, self._client_status.get('web', 'offline'))
|
||||
|
||||
def is_on_mobile(self):
|
||||
"""A helper function that determines if a member is active on a mobile device."""
|
||||
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
|
||||
return 'mobile' in self._client_status
|
||||
|
||||
@property
|
||||
@ -401,6 +407,11 @@ class Member(discord.abc.Messageable, _BaseUser):
|
||||
-----------
|
||||
message: :class:`Message`
|
||||
The message to check if you're mentioned in.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Indicates if the member is mentioned in the message.
|
||||
"""
|
||||
if message.guild is None or message.guild.id != self.guild.id:
|
||||
return False
|
||||
|
@ -208,6 +208,37 @@ class Attachment:
|
||||
data = await self.read(use_cached=use_cached)
|
||||
return File(io.BytesIO(data), filename=self.filename, spoiler=spoiler)
|
||||
|
||||
class MessageReference:
|
||||
"""Represents a reference to a :class:`Message`.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
message_id: Optional[:class:`int`]
|
||||
The id of the message referenced.
|
||||
channel_id: :class:`int`
|
||||
The channel id of the message referenced.
|
||||
guild_id: Optional[:class:`int`]
|
||||
The guild id of the message referenced.
|
||||
"""
|
||||
|
||||
__slots__ = ('message_id', 'channel_id', 'guild_id', '_state')
|
||||
|
||||
def __init__(self, state, **kwargs):
|
||||
self.message_id = utils._get_as_snowflake(kwargs, 'message_id')
|
||||
self.channel_id = int(kwargs.pop('channel_id'))
|
||||
self.guild_id = utils._get_as_snowflake(kwargs, 'guild_id')
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def cached_message(self):
|
||||
"""Optional[:class:`Message`]: The cached message, if found in the internal message cache."""
|
||||
return self._state._get_message(self.message_id)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MessageReference message_id={0.message_id!r} channel_id={0.channel_id!r} guild_id={0.guild_id!r}>'.format(self)
|
||||
|
||||
def flatten_handlers(cls):
|
||||
prefix = len('_handle_')
|
||||
cls._HANDLERS = {
|
||||
@ -251,6 +282,13 @@ class Message:
|
||||
call: Optional[:class:`CallMessage`]
|
||||
The call that the message refers to. This is only applicable to messages of type
|
||||
:attr:`MessageType.call`.
|
||||
reference: Optional[:class:`MessageReference`]
|
||||
The message that this message references. This is only applicable to messages of
|
||||
type :attr:`MessageType.pins_add` or crossposted messages created by a
|
||||
followed channel integration.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
mention_everyone: :class:`bool`
|
||||
Specifies if the message mentions everyone.
|
||||
|
||||
@ -316,7 +354,7 @@ class Message:
|
||||
'_cs_channel_mentions', '_cs_raw_mentions', 'attachments',
|
||||
'_cs_clean_content', '_cs_raw_channel_mentions', 'nonce', 'pinned',
|
||||
'role_mentions', '_cs_raw_role_mentions', 'type', 'call', 'flags',
|
||||
'_cs_system_content', '_cs_guild', '_state', 'reactions',
|
||||
'_cs_system_content', '_cs_guild', '_state', 'reactions', 'reference',
|
||||
'application', 'activity')
|
||||
|
||||
def __init__(self, *, state, channel, data):
|
||||
@ -338,6 +376,9 @@ class Message:
|
||||
self.content = data['content']
|
||||
self.nonce = data.get('nonce')
|
||||
|
||||
ref = data.get('message_reference')
|
||||
self.reference = MessageReference(state, **ref) if ref is not None else None
|
||||
|
||||
for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
|
||||
try:
|
||||
getattr(self, '_handle_%s' % handler)(data[handler])
|
||||
@ -476,8 +517,7 @@ class Message:
|
||||
author = self.author
|
||||
try:
|
||||
# Update member reference
|
||||
if author.joined_at is None:
|
||||
author.joined_at = utils.parse_time(member.get('joined_at'))
|
||||
author._update_from_message(member)
|
||||
except AttributeError:
|
||||
# It's a user here
|
||||
# TODO: consider adding to cache here
|
||||
@ -573,7 +613,7 @@ class Message:
|
||||
|
||||
@utils.cached_slot_property('_cs_clean_content')
|
||||
def clean_content(self):
|
||||
"""A property that returns the content in a "cleaned up"
|
||||
""":class:`str`: A property that returns the content in a "cleaned up"
|
||||
manner. This basically means that mentions are transformed
|
||||
into the way the client shows it. e.g. ``<#id>`` will transform
|
||||
into ``#name``.
|
||||
|
@ -124,11 +124,11 @@ class PartialEmoji(_EmojiTag):
|
||||
return hash((self.id, self.name))
|
||||
|
||||
def is_custom_emoji(self):
|
||||
"""Checks if this is a custom non-Unicode emoji."""
|
||||
""":class:`bool`: Checks if this is a custom non-Unicode emoji."""
|
||||
return self.id is not None
|
||||
|
||||
def is_unicode_emoji(self):
|
||||
"""Checks if this is a Unicode emoji."""
|
||||
""":class:`bool`: Checks if this is a Unicode emoji."""
|
||||
return self.id is None
|
||||
|
||||
def _as_reaction(self):
|
||||
|
@ -24,7 +24,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from .flags import BaseFlags, flag_value, fill_with_flags
|
||||
from .flags import BaseFlags, flag_value, fill_with_flags, alias_flag_value
|
||||
|
||||
__all__ = (
|
||||
'Permissions',
|
||||
@ -33,7 +33,7 @@ __all__ = (
|
||||
|
||||
# A permission alias works like a regular flag but is marked
|
||||
# So the PermissionOverwrite knows to work with it
|
||||
class permission_alias(flag_value):
|
||||
class permission_alias(alias_flag_value):
|
||||
pass
|
||||
|
||||
def make_permission_alias(alias):
|
||||
@ -131,14 +131,6 @@ class Permissions(BaseFlags):
|
||||
__lt__ = is_strict_subset
|
||||
__gt__ = is_strict_superset
|
||||
|
||||
def __iter__(self):
|
||||
for name, value in self.__class__.__dict__.items():
|
||||
if isinstance(value, permission_alias):
|
||||
continue
|
||||
|
||||
if isinstance(value, flag_value):
|
||||
yield (name, self._has_flag(value.flag))
|
||||
|
||||
@classmethod
|
||||
def none(cls):
|
||||
"""A factory method that creates a :class:`Permissions` with all
|
||||
@ -495,10 +487,7 @@ class PermissionOverwrite:
|
||||
self._values[key] = value
|
||||
|
||||
def pair(self):
|
||||
"""Returns the (allow, deny) pair from this overwrite.
|
||||
|
||||
The value of these pairs is :class:`Permissions`.
|
||||
"""
|
||||
"""Tuple[:class:`Permissions`, :class:`Permissions`]: Returns the (allow, deny) pair from this overwrite."""
|
||||
|
||||
allow = Permissions.none()
|
||||
deny = Permissions.none()
|
||||
@ -530,6 +519,11 @@ class PermissionOverwrite:
|
||||
|
||||
An empty permission overwrite is one that has no overwrites set
|
||||
to ``True`` or ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Indicates if the overwrite is empty.
|
||||
"""
|
||||
return all(x is None for x in self._values.values())
|
||||
|
||||
|
@ -148,7 +148,7 @@ class Role(Hashable):
|
||||
self.mentionable = data.get('mentionable', False)
|
||||
|
||||
def is_default(self):
|
||||
"""Checks if the role is the default role."""
|
||||
""":class:`bool`: Checks if the role is the default role."""
|
||||
return self.guild.id == self.id
|
||||
|
||||
@property
|
||||
|
@ -295,14 +295,11 @@ class AutoShardedClient(Client):
|
||||
elif not isinstance(self.shard_ids, (list, tuple)):
|
||||
raise ClientException('shard_ids parameter must be a list or a tuple.')
|
||||
|
||||
self._connection = AutoShardedConnectionState(dispatch=self.dispatch,
|
||||
handlers=self._handlers, syncer=self._syncer,
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **kwargs)
|
||||
|
||||
# instead of a single websocket, we have multiple
|
||||
# the key is the shard_id
|
||||
self.__shards = {}
|
||||
self._connection._get_websocket = self._get_websocket
|
||||
self._connection._get_client = lambda: self
|
||||
self.__queue = asyncio.PriorityQueue()
|
||||
|
||||
def _get_websocket(self, guild_id=None, *, shard_id=None):
|
||||
@ -310,6 +307,11 @@ class AutoShardedClient(Client):
|
||||
shard_id = (guild_id >> 22) % self.shard_count
|
||||
return self.__shards[shard_id].ws
|
||||
|
||||
def _get_state(self, **options):
|
||||
return AutoShardedConnectionState(dispatch=self.dispatch,
|
||||
handlers=self._handlers, syncer=self._syncer,
|
||||
hooks=self._hooks, http=self.http, loop=self.loop, **options)
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
|
||||
|
@ -32,6 +32,7 @@ import itertools
|
||||
import logging
|
||||
import math
|
||||
import weakref
|
||||
import warnings
|
||||
import inspect
|
||||
import gc
|
||||
|
||||
@ -82,6 +83,12 @@ class ChunkRequest:
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
async def logging_coroutine(coroutine, *, info):
|
||||
try:
|
||||
await coroutine
|
||||
except Exception:
|
||||
log.exception('Exception occurred during %s', info)
|
||||
|
||||
class ConnectionState:
|
||||
def __init__(self, *, dispatch, handlers, hooks, syncer, http, loop, **options):
|
||||
self.loop = loop
|
||||
@ -97,7 +104,6 @@ class ConnectionState:
|
||||
self.hooks = hooks
|
||||
self.shard_count = None
|
||||
self._ready_task = None
|
||||
self._fetch_offline = options.get('fetch_offline_members', True)
|
||||
self.heartbeat_timeout = options.get('heartbeat_timeout', 60.0)
|
||||
self.guild_ready_timeout = options.get('guild_ready_timeout', 2.0)
|
||||
if self.guild_ready_timeout < 0:
|
||||
@ -130,21 +136,31 @@ class ConnectionState:
|
||||
if intents is not None:
|
||||
if not isinstance(intents, Intents):
|
||||
raise TypeError('intents parameter must be Intent not %r' % type(intents))
|
||||
|
||||
if not intents.members and self._fetch_offline:
|
||||
raise ValueError('Intents.members has be enabled to fetch offline members.')
|
||||
|
||||
else:
|
||||
intents = Intents()
|
||||
intents = Intents.default()
|
||||
|
||||
try:
|
||||
chunk_guilds = options['fetch_offline_members']
|
||||
except KeyError:
|
||||
chunk_guilds = options.get('chunk_guilds_at_startup', intents.members)
|
||||
else:
|
||||
msg = 'fetch_offline_members is deprecated, use chunk_guilds_at_startup instead'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
||||
|
||||
self._chunk_guilds = chunk_guilds
|
||||
|
||||
# Ensure these two are set properly
|
||||
if not intents.members and self._chunk_guilds:
|
||||
raise ValueError('Intents.members must be enabled to chunk guilds at startup.')
|
||||
|
||||
cache_flags = options.get('member_cache_flags', None)
|
||||
if cache_flags is None:
|
||||
cache_flags = MemberCacheFlags.all()
|
||||
cache_flags = MemberCacheFlags.from_intents(intents)
|
||||
else:
|
||||
if not isinstance(cache_flags, MemberCacheFlags):
|
||||
raise TypeError('member_cache_flags parameter must be MemberCacheFlags not %r' % type(cache_flags))
|
||||
|
||||
cache_flags._verify_intents(intents)
|
||||
cache_flags._verify_intents(intents)
|
||||
|
||||
self._member_cache_flags = cache_flags
|
||||
self._activity = activity
|
||||
@ -215,6 +231,12 @@ class ConnectionState:
|
||||
u = self.user
|
||||
return u.id if u else None
|
||||
|
||||
@property
|
||||
def intents(self):
|
||||
ret = Intents.none()
|
||||
ret.value = self._intents.value
|
||||
return ret
|
||||
|
||||
@property
|
||||
def voice_clients(self):
|
||||
return list(self._voice_clients.values())
|
||||
@ -328,7 +350,7 @@ class ConnectionState:
|
||||
|
||||
def _guild_needs_chunking(self, guild):
|
||||
# If presences are enabled then we get back the old guild.large behaviour
|
||||
return self._fetch_offline and not guild.chunked and not (self._intents.presences and not guild.large)
|
||||
return self._chunk_guilds and not guild.chunked and not (self._intents.presences and not guild.large)
|
||||
|
||||
def _get_guild_channel(self, data):
|
||||
channel_id = int(data['channel_id'])
|
||||
@ -941,9 +963,8 @@ class ConnectionState:
|
||||
if int(data['user_id']) == self.user.id:
|
||||
voice = self._get_voice_client(guild.id)
|
||||
if voice is not None:
|
||||
ch = guild.get_channel(channel_id)
|
||||
if ch is not None:
|
||||
voice.channel = ch
|
||||
coro = voice.on_voice_state_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
||||
|
||||
member, before, after = guild._update_voice_state(data, channel_id)
|
||||
if member is not None:
|
||||
@ -971,7 +992,8 @@ class ConnectionState:
|
||||
|
||||
vc = self._get_voice_client(key_id)
|
||||
if vc is not None:
|
||||
asyncio.ensure_future(vc._create_socket(key_id, data))
|
||||
coro = vc.on_voice_server_update(data)
|
||||
asyncio.ensure_future(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||
|
||||
def parse_typing_start(self, data):
|
||||
channel, guild = self._get_guild_channel(data)
|
||||
|
@ -147,7 +147,7 @@ class BaseUser(_BaseUser):
|
||||
return str(self.avatar_url_as(static_format="png", size=1024))
|
||||
|
||||
def is_avatar_animated(self):
|
||||
"""Indicates if the user has an animated avatar."""
|
||||
""":class:`bool`: Indicates if the user has an animated avatar."""
|
||||
return bool(self.avatar and self.avatar.startswith('a_'))
|
||||
|
||||
def avatar_url_as(self, *, format=None, static_format='webp', size=1024):
|
||||
@ -259,6 +259,11 @@ class BaseUser(_BaseUser):
|
||||
-----------
|
||||
message: :class:`Message`
|
||||
The message to check if you're mentioned in.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Indicates if the user is mentioned in the message.
|
||||
"""
|
||||
|
||||
if message.mention_everyone:
|
||||
@ -703,6 +708,11 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
|
||||
This should be rarely called, as this is done transparently for most
|
||||
people.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`.DMChannel`
|
||||
The channel that was created.
|
||||
"""
|
||||
found = self.dm_channel
|
||||
if found is not None:
|
||||
@ -748,7 +758,7 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
return [User(state=state, data=friend) for friend in mutuals]
|
||||
|
||||
def is_friend(self):
|
||||
"""Checks if the user is your friend.
|
||||
""":class:`bool`: Checks if the user is your friend.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -760,7 +770,7 @@ class User(BaseUser, discord.abc.Messageable):
|
||||
return r.type is RelationshipType.friend
|
||||
|
||||
def is_blocked(self):
|
||||
"""Checks if the user is blocked.
|
||||
""":class:`bool`: Checks if the user is blocked.
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -45,7 +45,7 @@ import logging
|
||||
import struct
|
||||
import threading
|
||||
|
||||
from . import opus
|
||||
from . import opus, utils
|
||||
from .backoff import ExponentialBackoff
|
||||
from .gateway import *
|
||||
from .errors import ClientException, ConnectionClosed
|
||||
@ -59,7 +59,116 @@ except ImportError:
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class VoiceClient:
|
||||
class VoiceProtocol:
|
||||
"""A class that represents the Discord voice protocol.
|
||||
|
||||
This is an abstract class. The library provides a concrete implementation
|
||||
under :class:`VoiceClient`.
|
||||
|
||||
This class allows you to implement a protocol to allow for an external
|
||||
method of sending voice, such as Lavalink_ or a native library implementation.
|
||||
|
||||
These classes are passed to :meth:`abc.Connectable.connect`.
|
||||
|
||||
.. _Lavalink: https://github.com/Frederikam/Lavalink
|
||||
|
||||
Parameters
|
||||
------------
|
||||
client: :class:`Client`
|
||||
The client (or its subclasses) that started the connection request.
|
||||
channel: :class:`abc.Connectable`
|
||||
The voice channel that is being connected to.
|
||||
"""
|
||||
|
||||
def __init__(self, client, channel):
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
"""|coro|
|
||||
|
||||
An abstract method that is called when the client's voice state
|
||||
has changed. This corresponds to ``VOICE_STATE_UPDATE``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
data: :class:`dict`
|
||||
The raw `voice state payload`__.
|
||||
|
||||
.. _voice_state_update_payload: https://discord.com/developers/docs/resources/voice#voice-state-object
|
||||
|
||||
__ voice_state_update_payload_
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
"""|coro|
|
||||
|
||||
An abstract method that is called when initially connecting to voice.
|
||||
This corresponds to ``VOICE_SERVER_UPDATE``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
data: :class:`dict`
|
||||
The raw `voice server update payload`__.
|
||||
|
||||
.. _voice_server_update_payload: https://discord.com/developers/docs/topics/gateway#voice-server-update-voice-server-update-event-fields
|
||||
|
||||
__ voice_server_update_payload_
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def connect(self, *, timeout, reconnect):
|
||||
"""|coro|
|
||||
|
||||
An abstract method called when the client initiates the connection request.
|
||||
|
||||
When a connection is requested initially, the library calls the constructor
|
||||
under ``__init__`` and then calls :meth:`connect`. If :meth:`connect` fails at
|
||||
some point then :meth:`disconnect` is called.
|
||||
|
||||
Within this method, to start the voice connection flow it is recommended to
|
||||
use :meth:`Guild.change_voice_state` to start the flow. After which,
|
||||
:meth:`on_voice_server_update` and :meth:`on_voice_state_update` will be called.
|
||||
The order that these two are called is unspecified.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
timeout: :class:`float`
|
||||
The timeout for the connection.
|
||||
reconnect: :class:`bool`
|
||||
Whether reconnection is expected.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def disconnect(self, *, force):
|
||||
"""|coro|
|
||||
|
||||
An abstract method called when the client terminates the connection.
|
||||
|
||||
See :meth:`cleanup`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
force: :class:`bool`
|
||||
Whether the disconnection was forced.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def cleanup(self):
|
||||
"""This method *must* be called to ensure proper clean-up during a disconnect.
|
||||
|
||||
It is advisable to call this from within :meth:`disconnect` when you are
|
||||
completely done with the voice protocol instance.
|
||||
|
||||
This method removes it from the internal state cache that keeps track of
|
||||
currently alive voice clients. Failure to clean-up will cause subsequent
|
||||
connections to report that it's still connected.
|
||||
"""
|
||||
key_id, _ = self.channel._get_voice_client_key()
|
||||
self.client._connection._remove_voice_client(key_id)
|
||||
|
||||
class VoiceClient(VoiceProtocol):
|
||||
"""Represents a Discord voice connection.
|
||||
|
||||
You do not create these, you typically get them from
|
||||
@ -85,14 +194,13 @@ class VoiceClient:
|
||||
loop: :class:`asyncio.AbstractEventLoop`
|
||||
The event loop that the voice client is running on.
|
||||
"""
|
||||
def __init__(self, state, timeout, channel):
|
||||
def __init__(self, client, channel):
|
||||
if not has_nacl:
|
||||
raise RuntimeError("PyNaCl library needed in order to use voice")
|
||||
|
||||
self.channel = channel
|
||||
self.main_ws = None
|
||||
self.timeout = timeout
|
||||
self.ws = None
|
||||
super().__init__(client, channel)
|
||||
state = client._connection
|
||||
self.token = None
|
||||
self.socket = None
|
||||
self.loop = state.loop
|
||||
self._state = state
|
||||
@ -100,8 +208,8 @@ class VoiceClient:
|
||||
self._connected = threading.Event()
|
||||
|
||||
self._handshaking = False
|
||||
self._handshake_check = asyncio.Lock()
|
||||
self._handshake_complete = asyncio.Event()
|
||||
self._voice_state_complete = asyncio.Event()
|
||||
self._voice_server_complete = asyncio.Event()
|
||||
|
||||
self.mode = None
|
||||
self._connections = 0
|
||||
@ -138,48 +246,28 @@ class VoiceClient:
|
||||
|
||||
# connection related
|
||||
|
||||
async def start_handshake(self):
|
||||
log.info('Starting voice handshake...')
|
||||
async def on_voice_state_update(self, data):
|
||||
self.session_id = data['session_id']
|
||||
channel_id = data['channel_id']
|
||||
|
||||
guild_id, channel_id = self.channel._get_voice_state_pair()
|
||||
state = self._state
|
||||
self.main_ws = ws = state._get_websocket(guild_id)
|
||||
self._connections += 1
|
||||
if not self._handshaking:
|
||||
# If we're done handshaking then we just need to update ourselves
|
||||
if channel_id is None:
|
||||
# We're being disconnected so cleanup
|
||||
await self.disconnect()
|
||||
else:
|
||||
guild = self.guild
|
||||
self.channel = channel_id and guild and guild.get_channel(int(channel_id))
|
||||
else:
|
||||
self._voice_state_complete.set()
|
||||
|
||||
# request joining
|
||||
await ws.voice_state(guild_id, channel_id)
|
||||
async def on_voice_server_update(self, data):
|
||||
if self._voice_server_complete.is_set():
|
||||
log.info('Ignoring extraneous voice server update.')
|
||||
return
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self._handshake_complete.wait(), timeout=self.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
await self.terminate_handshake(remove=True)
|
||||
raise
|
||||
|
||||
log.info('Voice handshake complete. Endpoint found %s (IP: %s)', self.endpoint, self.endpoint_ip)
|
||||
|
||||
async def terminate_handshake(self, *, remove=False):
|
||||
guild_id, channel_id = self.channel._get_voice_state_pair()
|
||||
self._handshake_complete.clear()
|
||||
await self.main_ws.voice_state(guild_id, None, self_mute=True)
|
||||
self._handshaking = False
|
||||
|
||||
log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', channel_id, guild_id)
|
||||
if remove:
|
||||
log.info('The voice client has been removed for Channel ID %s (Guild ID %s)', channel_id, guild_id)
|
||||
key_id, _ = self.channel._get_voice_client_key()
|
||||
self._state._remove_voice_client(key_id)
|
||||
|
||||
async def _create_socket(self, server_id, data):
|
||||
async with self._handshake_check:
|
||||
if self._handshaking:
|
||||
log.info("Ignoring voice server update while handshake is in progress")
|
||||
return
|
||||
self._handshaking = True
|
||||
|
||||
self._connected.clear()
|
||||
self.session_id = self.main_ws.session_id
|
||||
self.server_id = server_id
|
||||
self.token = data.get('token')
|
||||
self.server_id = int(data['guild_id'])
|
||||
endpoint = data.get('endpoint')
|
||||
|
||||
if endpoint is None or self.token is None:
|
||||
@ -195,23 +283,77 @@ class VoiceClient:
|
||||
# This gets set later
|
||||
self.endpoint_ip = None
|
||||
|
||||
if self.socket:
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
if self._handshake_complete.is_set():
|
||||
# terminate the websocket and handle the reconnect loop if necessary.
|
||||
self._handshake_complete.clear()
|
||||
self._handshaking = False
|
||||
if not self._handshaking:
|
||||
# If we're not handshaking then we need to terminate our previous connection in the websocket
|
||||
await self.ws.close(4000)
|
||||
return
|
||||
|
||||
self._handshake_complete.set()
|
||||
self._voice_server_complete.set()
|
||||
|
||||
async def voice_connect(self):
|
||||
self._connections += 1
|
||||
await self.channel.guild.change_voice_state(channel=self.channel)
|
||||
|
||||
async def voice_disconnect(self):
|
||||
log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id)
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
|
||||
async def connect(self, *, reconnect, timeout):
|
||||
log.info('Connecting to voice...')
|
||||
self.timeout = timeout
|
||||
try:
|
||||
del self.secret_key
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
for i in range(5):
|
||||
self._voice_state_complete.clear()
|
||||
self._voice_server_complete.clear()
|
||||
self._handshaking = True
|
||||
|
||||
# This has to be created before we start the flow.
|
||||
futures = [
|
||||
self._voice_state_complete.wait(),
|
||||
self._voice_server_complete.wait(),
|
||||
]
|
||||
|
||||
# Start the connection flow
|
||||
log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1)
|
||||
await self.voice_connect()
|
||||
|
||||
try:
|
||||
await utils.sane_wait_for(futures, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
await self.disconnect(force=True)
|
||||
raise
|
||||
|
||||
log.info('Voice handshake complete. Endpoint found %s', self.endpoint)
|
||||
self._handshaking = False
|
||||
self._voice_server_complete.clear()
|
||||
self._voice_state_complete.clear()
|
||||
|
||||
try:
|
||||
self.ws = await DiscordVoiceWebSocket.from_client(self)
|
||||
self._connected.clear()
|
||||
while not hasattr(self, 'secret_key'):
|
||||
await self.ws.poll_event()
|
||||
self._connected.set()
|
||||
break
|
||||
except (ConnectionClosed, asyncio.TimeoutError):
|
||||
if reconnect:
|
||||
log.exception('Failed to connect to voice... Retrying...')
|
||||
await asyncio.sleep(1 + i * 2.0)
|
||||
await self.voice_disconnect()
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if self._runner is None:
|
||||
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
@ -234,35 +376,6 @@ class VoiceClient:
|
||||
ws = self.ws
|
||||
return float("inf") if not ws else ws.average_latency
|
||||
|
||||
async def connect(self, *, reconnect=True, _tries=0, do_handshake=True):
|
||||
log.info('Connecting to voice...')
|
||||
try:
|
||||
del self.secret_key
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if do_handshake:
|
||||
await self.start_handshake()
|
||||
|
||||
try:
|
||||
self.ws = await DiscordVoiceWebSocket.from_client(self)
|
||||
self._handshaking = False
|
||||
self._connected.clear()
|
||||
while not hasattr(self, 'secret_key'):
|
||||
await self.ws.poll_event()
|
||||
self._connected.set()
|
||||
except (ConnectionClosed, asyncio.TimeoutError):
|
||||
if reconnect and _tries < 5:
|
||||
log.exception('Failed to connect to voice... Retrying...')
|
||||
await asyncio.sleep(1 + _tries * 2.0)
|
||||
await self.terminate_handshake()
|
||||
await self.connect(reconnect=reconnect, _tries=_tries + 1)
|
||||
else:
|
||||
raise
|
||||
|
||||
if self._runner is None:
|
||||
self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
|
||||
|
||||
async def poll_voice_ws(self, reconnect):
|
||||
backoff = ExponentialBackoff()
|
||||
while True:
|
||||
@ -287,9 +400,9 @@ class VoiceClient:
|
||||
log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry)
|
||||
self._connected.clear()
|
||||
await asyncio.sleep(retry)
|
||||
await self.terminate_handshake()
|
||||
await self.voice_disconnect()
|
||||
try:
|
||||
await self.connect(reconnect=True)
|
||||
await self.connect(reconnect=True, timeout=self.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
# at this point we've retried 5 times... let's continue the loop.
|
||||
log.warning('Could not connect to voice... Retrying...')
|
||||
@ -310,8 +423,9 @@ class VoiceClient:
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
|
||||
await self.terminate_handshake(remove=True)
|
||||
await self.voice_disconnect()
|
||||
finally:
|
||||
self.cleanup()
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
|
||||
@ -325,8 +439,7 @@ class VoiceClient:
|
||||
channel: :class:`abc.Snowflake`
|
||||
The channel to move to. Must be a voice channel.
|
||||
"""
|
||||
guild_id, _ = self.channel._get_voice_state_pair()
|
||||
await self.main_ws.voice_state(guild_id, channel.id)
|
||||
await self.channel.guild.change_voice_state(channel=channel)
|
||||
|
||||
def is_connected(self):
|
||||
"""Indicates if the voice client is connected to voice."""
|
||||
|
@ -29,9 +29,8 @@ from .user import BaseUser
|
||||
from .activity import create_activity
|
||||
from .invite import Invite
|
||||
from .enums import Status, try_enum
|
||||
from collections import namedtuple
|
||||
|
||||
class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
|
||||
class WidgetChannel:
|
||||
"""Represents a "partial" widget channel.
|
||||
|
||||
.. container:: operations
|
||||
@ -61,11 +60,20 @@ class WidgetChannel(namedtuple('WidgetChannel', 'id name position')):
|
||||
position: :class:`int`
|
||||
The channel's position
|
||||
"""
|
||||
__slots__ = ()
|
||||
__slots__ = ('id', 'name', 'position')
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop('id')
|
||||
self.name = kwargs.pop('name')
|
||||
self.position = kwargs.pop('position')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return '<WidgetChannel id={0.id} name={0.name!r} position={0.position!r}>'.format(self)
|
||||
|
||||
@property
|
||||
def mention(self):
|
||||
""":class:`str`: The string that allows you to mention the channel."""
|
||||
|
Reference in New Issue
Block a user