Merge branch 'Rapptz-feature/intents'

This commit is contained in:
iDutchy 2020-09-15 00:37:04 +00:00
commit 5756548a6a
10 changed files with 239 additions and 56 deletions

View File

@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
""" """
import abc import abc
import sys
import copy import copy
import asyncio import asyncio
@ -169,7 +170,7 @@ class _Overwrites:
self.id = kwargs.pop('id') self.id = kwargs.pop('id')
self.allow = kwargs.pop('allow', 0) self.allow = kwargs.pop('allow', 0)
self.deny = kwargs.pop('deny', 0) self.deny = kwargs.pop('deny', 0)
self.type = kwargs.pop('type') self.type = sys.intern(kwargs.pop('type'))
def _asdict(self): def _asdict(self):
return { return {
@ -933,7 +934,6 @@ class Messageable(metaclass=abc.ABCMeta):
""" """
return Typing(self) return Typing(self)
@utils.deprecated('fetch_message_fast')
async def fetch_message(self, id): async def fetch_message(self, id):
"""|coro| """|coro|
@ -941,8 +941,6 @@ class Messageable(metaclass=abc.ABCMeta):
This can only be used by bot accounts. This can only be used by bot accounts.
Prefer using :meth:`fetch_message_fast`.
Parameters Parameters
------------ ------------
id: :class:`int` id: :class:`int`
@ -967,40 +965,6 @@ class Messageable(metaclass=abc.ABCMeta):
data = await self._state.http.get_message(channel.id, id) data = await self._state.http.get_message(channel.id, id)
return self._state.create_message(channel=channel, data=data) return self._state.create_message(channel=channel, data=data)
async def fetch_message_fast(self, id):
"""|coro|
Retrieves a single :class:`~discord.Message` from the destination, using
the history endpoint.
.. versionadded:: 1.5
Parameters
------------
id: :class:`int`
The message ID to look for.
Raises
--------
~discord.NotFound
The specified channel was not found.
~discord.Forbidden
You do not have permissions to get channel message history.
~discord.HTTPException
The request to get message history failed.
Returns
--------
Optional[:class:`~discord.Message`]
The message asked for, or None if there is no match.
"""
channel = await self._get_channel()
data = await self._state.http.logs_from(channel.id, limit=1, around=id)
if data and int(data[0]['id']) == id:
return self._state.create_message(channel=channel, data=data[0])
return None
async def pins(self): async def pins(self):
"""|coro| """|coro|

View File

@ -142,9 +142,14 @@ class Client:
shard_count: Optional[:class:`int`] shard_count: Optional[:class:`int`]
The total number of shards. The total number of shards.
intents: :class:`Intents` intents: :class:`Intents`
A list of 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.
Currently, if no intents are passed then you will receive all data.
.. versionadded:: 1.5
member_cache_flags: :class:`MemberCacheFlags`
Allows for finer control over how the library caches members.
.. 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 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 members from the guilds the client belongs to. If this is ``False``\, then
@ -565,6 +570,8 @@ class Client:
# sometimes, discord sends us 1000 for unknown reasons so we should reconnect # sometimes, discord sends us 1000 for unknown reasons so we should reconnect
# regardless and rely on is_closed instead # regardless and rely on is_closed instead
if isinstance(exc, ConnectionClosed): if isinstance(exc, ConnectionClosed):
if exc.code == 4014:
raise PrivilegedIntentsRequired(exc.shard_id) from None
if exc.code != 1000: if exc.code != 1000:
await self.close() await self.close()
raise raise

View File

@ -51,7 +51,7 @@ __all__ = (
'Theme', 'Theme',
'WebhookType', 'WebhookType',
'ExpireBehaviour', 'ExpireBehaviour',
'ExpireBehavior' 'ExpireBehavior',
) )
def _create_value_cls(name): def _create_value_cls(name):

View File

@ -175,3 +175,27 @@ class ConnectionClosed(ClientException):
self.reason = '' self.reason = ''
self.shard_id = shard_id self.shard_id = shard_id
super().__init__('Shard ID %s WebSocket closed with %s' % (self.shard_id, self.code)) super().__init__('Shard ID %s WebSocket closed with %s' % (self.shard_id, self.code))
class PrivilegedIntentsRequired(ClientException):
"""Exception that's thrown when the gateway is requesting privileged intents
but they're not ticked in the developer page yet.
Go to https://discord.com/developers/applications/ and enable the intents
that are required. Currently these are as follows:
- :attr:`Intents.members`
- :attr:`Intents.presences`
Attributes
-----------
shard_id: Optional[:class:`int`]
The shard ID that got closed if applicable.
"""
def __init__(self, shard_id):
self.shard_id = shard_id
msg = 'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the ' \
'developer portal. It is recommended to go to https://discord.com/developers/applications/ ' \
'and explicitly enable the privileged intents within your application\'s page. If this is not ' \
'possible, then consider disabling the privileged intents instead.'
super().__init__(msg % shard_id)

View File

@ -31,6 +31,7 @@ __all__ = (
'MessageFlags', 'MessageFlags',
'PublicUserFlags', 'PublicUserFlags',
'Intents', 'Intents',
'MemberCacheFlags',
) )
class flag_value: class flag_value:
@ -651,3 +652,127 @@ class Intents(BaseFlags):
- :func:`on_typing` (only for DMs) - :func:`on_typing` (only for DMs)
""" """
return 1 << 14 return 1 << 14
@fill_with_flags()
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.
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`
enabled. Otherwise the library cannot know when a member leaves a guild and
is thus unable to cleanup after itself.
To construct an object you can pass keyword arguments denoting the flags
to enable or disable.
The default value is all flags enabled.
.. versionadded:: 1.5
.. container:: operations
.. describe:: x == y
Checks if two flags are equal.
.. describe:: x != y
Checks if two flags 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.
Attributes
-----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
__slots__ = ()
def __init__(self, **kwargs):
bits = max(self.VALID_FLAGS.values()).bit_length()
self.value = (1 << bits) - 1
for key, value in kwargs.items():
if key not in self.VALID_FLAGS:
raise TypeError('%r is not a valid flag name.' % key)
setattr(self, key, value)
@classmethod
def all(cls):
"""A factory method that creates a :class:`MemberCacheFlags` with everything enabled."""
bits = max(cls.VALID_FLAGS.values()).bit_length()
value = (1 << bits) - 1
self = cls.__new__(cls)
self.value = value
return self
@classmethod
def none(cls):
"""A factory method that creates a :class:`MemberCacheFlags` with everything disabled."""
self = cls.__new__(cls)
self.value = self.DEFAULT_VALUE
return self
@flag_value
def online(self):
""":class:`bool`: Whether to cache members with a status.
For example, members that are part of the initial ``GUILD_CREATE``
or become online at a later point. This requires :attr:`Intents.presences`.
Members that go offline are no longer cached.
"""
return 1
@flag_value
def voice(self):
""":class:`bool`: Whether to cache members that are in voice.
This requires :attr:`Intents.voice_states`.
Members that leave voice are no longer cached.
"""
return 2
@flag_value
def joined(self):
""":class:`bool`: Whether to cache members that joined the guild
or are chunked as part of the initial log in flow.
This requires :attr:`Intents.members`.
Members that leave the guild are no longer cached.
"""
return 4
def _verify_intents(self, intents):
if self.online and not intents.presences:
raise ValueError('MemberCacheFlags.online requires Intents.presences enabled')
if self.voice and not intents.voice_states:
raise ValueError('MemberCacheFlags.voice requires Intents.voice_states')
if self.joined and not intents.members:
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 ' \
'to properly evict members from the cache.'
raise ValueError(msg)
@property
def _voice_only(self):
return self.value == 2
@property
def _online_only(self):
return self.value == 1

View File

@ -305,11 +305,12 @@ class Guild(Hashable):
self._rules_channel_id = utils._get_as_snowflake(guild, 'rules_channel_id') self._rules_channel_id = utils._get_as_snowflake(guild, 'rules_channel_id')
self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id') self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id')
cache_members = self._state._cache_members cache_online_members = self._state._member_cache_flags.online
cache_joined = self._state._member_cache_flags.joined
self_id = self._state.self_id self_id = self._state.self_id
for mdata in guild.get('members', []): for mdata in guild.get('members', []):
member = Member(data=mdata, guild=self, state=state) member = Member(data=mdata, guild=self, state=state)
if cache_members or member.id == self_id: if cache_joined or (cache_online_members and member.raw_status != 'offline') or member.id == self_id:
self._add_member(member) self._add_member(member)
self._sync(guild) self._sync(guild)

View File

@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
""" """
import itertools import itertools
import sys
from operator import attrgetter from operator import attrgetter
import discord.abc import discord.abc
@ -215,10 +216,10 @@ class Member(discord.abc.Messageable, _BaseUser):
clone = cls(data=data, guild=guild, state=state) clone = cls(data=data, guild=guild, state=state)
to_return = cls(data=data, guild=guild, state=state) to_return = cls(data=data, guild=guild, state=state)
to_return._client_status = { to_return._client_status = {
key: value sys.intern(key): sys.intern(value)
for key, value in data.get('client_status', {}).items() for key, value in data.get('client_status', {}).items()
} }
to_return._client_status[None] = data['status'] to_return._client_status[None] = sys.intern(data['status'])
return to_return, clone return to_return, clone
@classmethod @classmethod
@ -260,10 +261,10 @@ class Member(discord.abc.Messageable, _BaseUser):
def _presence_update(self, data, user): def _presence_update(self, data, user):
self.activities = tuple(map(create_activity, data.get('activities', []))) self.activities = tuple(map(create_activity, data.get('activities', [])))
self._client_status = { self._client_status = {
key: value sys.intern(key): sys.intern(value)
for key, value in data.get('client_status', {}).items() for key, value in data.get('client_status', {}).items()
} }
self._client_status[None] = data['status'] self._client_status[None] = sys.intern(data['status'])
if len(user) > 1: if len(user) > 1:
return self._update_inner_user(user) return self._update_inner_user(user)
@ -285,6 +286,14 @@ class Member(discord.abc.Messageable, _BaseUser):
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
return try_enum(Status, self._client_status[None]) return try_enum(Status, self._client_status[None])
@property
def raw_status(self):
""":class:`str`: The member's overall status as a string value.
.. versionadded:: 1.5
"""
return self._client_status[None]
@status.setter @status.setter
def status(self, value): def status(self, value):
# internal use only # internal use only

View File

@ -34,7 +34,15 @@ from .state import AutoShardedConnectionState
from .client import Client from .client import Client
from .backoff import ExponentialBackoff from .backoff import ExponentialBackoff
from .gateway import * from .gateway import *
from .errors import ClientException, InvalidArgument, HTTPException, GatewayNotFound, ConnectionClosed from .errors import (
ClientException,
InvalidArgument,
HTTPException,
GatewayNotFound,
ConnectionClosed,
PrivilegedIntentsRequired,
)
from . import utils from . import utils
from .enums import Status from .enums import Status
@ -125,6 +133,9 @@ class Shard:
return return
if isinstance(e, ConnectionClosed): if isinstance(e, ConnectionClosed):
if e.code == 4014:
self._queue_put(EventItem(EventType.terminate, self, PrivilegedIntentsRequired(self.id)))
return
if e.code != 1000: if e.code != 1000:
self._queue_put(EventItem(EventType.close, self, e)) self._queue_put(EventItem(EventType.close, self, e))
return return
@ -407,8 +418,11 @@ class AutoShardedClient(Client):
item = await self.__queue.get() item = await self.__queue.get()
if item.type == EventType.close: if item.type == EventType.close:
await self.close() await self.close()
if isinstance(item.error, ConnectionClosed) and item.error.code != 1000: if isinstance(item.error, ConnectionClosed):
raise item.error if item.error.code != 1000:
raise item.error
if item.error.code == 4014:
raise PrivilegedIntentsRequired(item.shard.id) from None
return return
elif item.type in (EventType.identify, EventType.resume): elif item.type in (EventType.identify, EventType.resume):
await item.shard.reidentify(item.error) await item.shard.reidentify(item.error)

View File

@ -51,7 +51,7 @@ from .member import Member
from .role import Role from .role import Role
from .enums import ChannelType, try_enum, Status from .enums import ChannelType, try_enum, Status
from . import utils from . import utils
from .flags import Intents from .flags import Intents, MemberCacheFlags
from .embeds import Embed from .embeds import Embed
from .object import Object from .object import Object
from .invite import Invite from .invite import Invite
@ -110,8 +110,6 @@ class ConnectionState:
raise TypeError('allowed_mentions parameter must be AllowedMentions') raise TypeError('allowed_mentions parameter must be AllowedMentions')
self.allowed_mentions = allowed_mentions self.allowed_mentions = allowed_mentions
# Only disable cache if both fetch_offline and guild_subscriptions are off.
self._cache_members = (self._fetch_offline or self.guild_subscriptions)
self._chunk_requests = [] self._chunk_requests = []
activity = options.get('activity', None) activity = options.get('activity', None)
@ -136,6 +134,19 @@ class ConnectionState:
if not intents.members and self._fetch_offline: if not intents.members and self._fetch_offline:
raise ValueError('Intents.members has be enabled to fetch offline members.') raise ValueError('Intents.members has be enabled to fetch offline members.')
else:
intents = Intents()
cache_flags = options.get('member_cache_flags', None)
if cache_flags is None:
cache_flags = MemberCacheFlags.all()
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)
self._member_cache_flags = cache_flags
self._activity = activity self._activity = activity
self._status = status self._status = status
self._intents = intents self._intents = intents
@ -558,6 +569,7 @@ class ConnectionState:
user = data['user'] user = data['user']
member_id = int(user['id']) member_id = int(user['id'])
member = guild.get_member(member_id) member = guild.get_member(member_id)
flags = self._member_cache_flags
if member is None: if member is None:
if 'username' not in user: if 'username' not in user:
# sometimes we receive 'incomplete' member data post-removal. # sometimes we receive 'incomplete' member data post-removal.
@ -565,13 +577,17 @@ class ConnectionState:
return return
member, old_member = Member._from_presence_update(guild=guild, data=data, state=self) member, old_member = Member._from_presence_update(guild=guild, data=data, state=self)
guild._add_member(member) if flags.online or (flags._online_only and member.raw_status != 'offline'):
guild._add_member(member)
else: else:
old_member = Member._copy(member) old_member = Member._copy(member)
user_update = member._presence_update(data=data, user=user) user_update = member._presence_update(data=data, user=user)
if user_update: if user_update:
self.dispatch('user_update', user_update[0], user_update[1]) self.dispatch('user_update', user_update[0], user_update[1])
if flags._online_only and member.raw_status == 'offline':
guild._remove_member(member)
self.dispatch('member_update', old_member, member) self.dispatch('member_update', old_member, member)
def parse_user_update(self, data): def parse_user_update(self, data):
@ -691,7 +707,7 @@ class ConnectionState:
return return
member = Member(guild=guild, data=data, state=self) member = Member(guild=guild, data=data, state=self)
if self._cache_members: if self._member_cache_flags.joined:
guild._add_member(member) guild._add_member(member)
guild._member_count += 1 guild._member_count += 1
self.dispatch('member_join', member) self.dispatch('member_join', member)
@ -754,7 +770,7 @@ class ConnectionState:
return self._add_guild_from_data(data) return self._add_guild_from_data(data)
async def chunk_guild(self, guild, *, wait=True, cache=None): async def chunk_guild(self, guild, *, wait=True, cache=None):
cache = cache or self._cache_members cache = cache or self._member_cache_flags.joined
future = self.loop.create_future() future = self.loop.create_future()
request = ChunkRequest(guild.id, future, self._get_guild, cache=cache) request = ChunkRequest(guild.id, future, self._get_guild, cache=cache)
self._chunk_requests.append(request) self._chunk_requests.append(request)
@ -920,6 +936,7 @@ class ConnectionState:
def parse_voice_state_update(self, data): def parse_voice_state_update(self, data):
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id')) guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
channel_id = utils._get_as_snowflake(data, 'channel_id') channel_id = utils._get_as_snowflake(data, 'channel_id')
flags = self._member_cache_flags
if guild is not None: if guild is not None:
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)
@ -930,6 +947,13 @@ class ConnectionState:
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:
if flags.voice:
if channel_id is None and flags.value == MemberCacheFlags.voice.flag:
# Only remove from cache iff we only have the voice flag enabled
guild._remove_member(member)
else:
guild._add_member(member)
self.dispatch('voice_state_update', member, before, after) self.dispatch('voice_state_update', member, before, after)
else: else:
log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id']) log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id'])

View File

@ -2803,6 +2803,18 @@ AllowedMentions
.. autoclass:: AllowedMentions .. autoclass:: AllowedMentions
:members: :members:
Intents
~~~~~~~~~~
.. autoclass:: Intents
:members:
MemberCacheFlags
~~~~~~~~~~~~~~~~~~
.. autoclass:: MemberCacheFlags
:members:
File File
~~~~~ ~~~~~
@ -2912,6 +2924,8 @@ The following exceptions are thrown by the library.
.. autoexception:: ConnectionClosed .. autoexception:: ConnectionClosed
.. autoexception:: PrivilegedIntentsRequired
.. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusError
.. autoexception:: discord.opus.OpusNotLoaded .. autoexception:: discord.opus.OpusNotLoaded
@ -2928,6 +2942,7 @@ Exception Hierarchy
- :exc:`InvalidArgument` - :exc:`InvalidArgument`
- :exc:`LoginFailure` - :exc:`LoginFailure`
- :exc:`ConnectionClosed` - :exc:`ConnectionClosed`
- :exc:`PrivilegedIntentsRequired`
- :exc:`NoMoreItems` - :exc:`NoMoreItems`
- :exc:`GatewayNotFound` - :exc:`GatewayNotFound`
- :exc:`HTTPException` - :exc:`HTTPException`