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`]
The total number of shards.
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.
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`
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

2
discord/enums.py

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

126
discord/flags.py

@ -31,6 +31,7 @@ __all__ = (
'MessageFlags',
'PublicUserFlags',
'Intents',
'MemberCacheFlags',
)
class flag_value:
@ -651,3 +652,128 @@ class Intents(BaseFlags):
- :func:`on_typing` (only for DMs)
"""
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._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
for mdata in guild.get('members', []):
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._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."""
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
def status(self, value):
# internal use only

33
discord/state.py

@ -51,7 +51,7 @@ from .member import Member
from .role import Role
from .enums import ChannelType, try_enum, Status
from . import utils
from .flags import Intents
from .flags import Intents, MemberCacheFlags
from .embeds import Embed
from .object import Object
from .invite import Invite
@ -116,8 +116,6 @@ class ConnectionState:
raise TypeError('allowed_mentions parameter must be AllowedMentions')
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 = []
activity = options.get('activity', None)
@ -142,6 +140,16 @@ class ConnectionState:
if not intents.members and self._fetch_offline:
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._status = status
self._intents = intents
@ -564,6 +572,7 @@ class ConnectionState:
user = data['user']
member_id = int(user['id'])
member = guild.get_member(member_id)
flags = self._member_cache_flags
if member is None:
if 'username' not in user:
# sometimes we receive 'incomplete' member data post-removal.
@ -571,13 +580,17 @@ class ConnectionState:
return
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:
old_member = Member._copy(member)
user_update = member._presence_update(data=data, user=user)
if user_update:
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)
def parse_user_update(self, data):
@ -697,7 +710,7 @@ class ConnectionState:
return
member = Member(guild=guild, data=data, state=self)
if self._cache_members:
if self._member_cache_flags.joined:
guild._add_member(member)
guild._member_count += 1
self.dispatch('member_join', member)
@ -760,7 +773,7 @@ class ConnectionState:
return self._add_guild_from_data(data)
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()
request = ChunkRequest(guild.id, future, self._get_guild, cache=cache)
self._chunk_requests.append(request)
@ -926,6 +939,7 @@ class ConnectionState:
def parse_voice_state_update(self, data):
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
channel_id = utils._get_as_snowflake(data, 'channel_id')
flags = self._member_cache_flags
if guild is not None:
if int(data['user_id']) == self.user.id:
voice = self._get_voice_client(guild.id)
@ -935,6 +949,13 @@ class ConnectionState:
member, before, after = guild._update_voice_state(data, channel_id)
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)
else:
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
:members:
Intents
~~~~~~~~~~
.. autoclass:: Intents
:members:
MemberCacheFlags
~~~~~~~~~~~~~~~~~~
.. autoclass:: MemberCacheFlags
:members:
File
~~~~~

Loading…
Cancel
Save