Browse Source

Allow finer grained control over the member cache.

pull/5849/head
Rapptz 5 years ago
parent
commit
23ae084b8c
  1. 9
      discord/client.py
  2. 2
      discord/enums.py
  3. 126
      discord/flags.py
  4. 5
      discord/guild.py
  5. 8
      discord/member.py
  6. 33
      discord/state.py
  7. 12
      docs/api.rst

9
discord/client.py

@ -141,9 +141,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

2
discord/enums.py

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

126
discord/flags.py

@ -31,6 +31,7 @@ __all__ = (
'MessageFlags', 'MessageFlags',
'PublicUserFlags', 'PublicUserFlags',
'Intents', 'Intents',
'MemberCacheFlags',
) )
class flag_value: class flag_value:
@ -651,3 +652,128 @@ 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

5
discord/guild.py

@ -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)

8
discord/member.py

@ -291,6 +291,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

33
discord/state.py

@ -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
@ -116,8 +116,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)
@ -142,6 +140,16 @@ 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.')
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
@ -564,6 +572,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.
@ -571,13 +580,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):
@ -697,7 +710,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)
@ -760,7 +773,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)
@ -926,6 +939,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)
@ -935,6 +949,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'])

12
docs/api.rst

@ -2816,6 +2816,18 @@ AllowedMentions
.. autoclass:: AllowedMentions .. autoclass:: AllowedMentions
:members: :members:
Intents
~~~~~~~~~~
.. autoclass:: Intents
:members:
MemberCacheFlags
~~~~~~~~~~~~~~~~~~
.. autoclass:: MemberCacheFlags
:members:
File File
~~~~~ ~~~~~

Loading…
Cancel
Save