From 8dbe0421f6545cdfc3b6e67bc4454ce130543478 Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 27 Dec 2019 23:33:58 +0000 Subject: [PATCH] Add user_id_or_ids and presences to guild member chunk and improve cache behaviour. (#162) * request_guild_members update, logging on emitter event exceptions and improved state caching. * Update the GuildMembersChunk GatewayEvent * Match presence update's presence handling in guild members chunk * Remove unnecessary string conversion * Remove member count tracking. * Split function * Remove optional report --- disco/api/client.py | 12 +----------- disco/gateway/client.py | 15 ++++++++++++++- disco/gateway/events.py | 6 ++++++ disco/state.py | 14 ++++++++++++++ disco/types/guild.py | 7 +++++-- disco/util/emitter.py | 23 ++++++++++++++++++----- disco/util/functional.py | 15 ++++++++++++--- 7 files changed, 70 insertions(+), 22 deletions(-) diff --git a/disco/api/client.py b/disco/api/client.py index ae75622..313a4bb 100644 --- a/disco/api/client.py +++ b/disco/api/client.py @@ -1,4 +1,3 @@ -import six import json import warnings @@ -7,6 +6,7 @@ from gevent.local import local from six.moves.urllib.parse import quote from disco.api.http import Routes, HTTPClient, to_bytes +from disco.util.functional import optional from disco.util.logging import LoggingClass from disco.util.sanitize import S from disco.types.user import User @@ -22,16 +22,6 @@ from disco.types.voice import VoiceRegion from disco.types.webhook import Webhook -def optional(**kwargs): - """ - Takes a set of keyword arguments, creating a dictionary with only the non- - null values. - - :returns: dict - """ - return {k: v for k, v in six.iteritems(kwargs) if v is not None} - - def _reason_header(value): return optional(**{'X-Audit-Log-Reason': quote(to_bytes(value)) if value else None}) diff --git a/disco/gateway/client.py b/disco/gateway/client.py index b778201..ce9fd79 100644 --- a/disco/gateway/client.py +++ b/disco/gateway/client.py @@ -273,7 +273,7 @@ class GatewayClient(LoggingClass): gevent.spawn(self.connect_and_run) self.ws_event.wait() - def request_guild_members(self, guild_id_or_ids, query=None, limit=0): + def request_guild_members(self, guild_id_or_ids, query=None, limit=0, presences=False): """ Request a batch of Guild members from Discord. Generally this function can be called when initially loading Guilds to fill the local member state. @@ -281,6 +281,19 @@ class GatewayClient(LoggingClass): self.send(OPCode.REQUEST_GUILD_MEMBERS, { # This is simply unfortunate naming on the part of Discord... 'guild_id': guild_id_or_ids, + 'limit': limit, + 'presences': presences, 'query': query or '', + }) + + def request_guild_members_by_id(self, guild_id_or_ids, user_id_or_ids, limit=0, presences=False): + """ + Request a batch of Guild members from Discord by their snowflake(s). + """ + self.send(OPCode.REQUEST_GUILD_MEMBERS, { + 'guild_id': guild_id_or_ids, 'limit': limit, + 'presences': presences, + # This is simply even more unfortunate naming from Discord... + 'user_ids': user_id_or_ids, }) diff --git a/disco/gateway/events.py b/disco/gateway/events.py index 61809fd..2d33a52 100644 --- a/disco/gateway/events.py +++ b/disco/gateway/events.py @@ -338,9 +338,15 @@ class GuildMembersChunk(GatewayEvent): The ID of the guild this member chunk is for. members : list[:class:`disco.types.guild.GuildMember`] The chunk of members. + not_found : list[snowflake] + An array of invalid requested guild members. + presences : list[:class:`disco.types.user.Presence`] + An array of requested member presence states. """ guild_id = Field(snowflake) members = ListField(GuildMember) + not_found = ListField(snowflake) + presences = ListField(Presence) @property def guild(self): diff --git a/disco/state.py b/disco/state.py index 0aa8888..2084be3 100644 --- a/disco/state.py +++ b/disco/state.py @@ -229,6 +229,11 @@ class State(object): elif event.channel.is_dm: self.dms[event.channel.id] = event.channel self.channels[event.channel.id] = event.channel + for user in six.itervalues(event.channel.recipients): + if user.id not in self.users: + self.users[user.id] = user + else: + event.channel.recipients[user.id] = self.users[user.id] def on_channel_update(self, event): if event.channel.id in self.channels: @@ -325,6 +330,15 @@ class State(object): else: member.user = self.users[member.id] + if not event.presences: + return + + for presence in event.presences: + # TODO: this matches the recursive, hackfix method found in on_presence_update + user = presence.user + user.presence = presence + self.users[user.id].inplace_update(user) + def on_guild_role_create(self, event): if event.guild_id not in self.guilds: return diff --git a/disco/types/guild.py b/disco/types/guild.py index 9a55e87..c220cba 100644 --- a/disco/types/guild.py +++ b/disco/types/guild.py @@ -455,8 +455,11 @@ class Guild(SlottedModel, Permissible): return self.client.api.guilds_roles_modify(self.id, to_snowflake(role), **kwargs) - def request_guild_members(self, query=None, limit=0): - self.client.gw.request_guild_members(self.id, query, limit) + def request_guild_members(self, query=None, limit=0, presences=False): + self.client.gw.request_guild_members(self.id, query, limit, presences) + + def request_guild_members_by_id(self, user_id_or_ids, limit=0, presences=False): + self.client.gw.request_guild_members_by_id(self.id, user_id_or_ids, limit, presences) def sync(self): warnings.warn( diff --git a/disco/util/emitter.py b/disco/util/emitter.py index 4382d4f..e5122e8 100644 --- a/disco/util/emitter.py +++ b/disco/util/emitter.py @@ -5,6 +5,9 @@ from gevent.event import AsyncResult from gevent.queue import Queue, Full +from disco.util.logging import LoggingClass + + class Priority(object): # BEFORE is the most dangerous priority level. Every event that flows through # the given emitter instance will be dispatched _sequentially_ to all BEFORE @@ -106,7 +109,7 @@ class EmitterSubscription(object): self.detach(emitter) -class Emitter(object): +class Emitter(LoggingClass): def __init__(self): self.event_handlers = { k: defaultdict(list) for k in Priority.ALL @@ -117,15 +120,25 @@ class Emitter(object): for listener in self.event_handlers[Priority.BEFORE].get(name, []): try: listener(*args, **kwargs) - except Exception: - pass + except Exception as e: + self.log.warning('BEFORE {} event handler `{}` raised {}: {}'.format( + name, + listener.callback.__name__, + e.__class__.__name__, + e, + )) # Next execute all AFTER handlers sequentially for listener in self.event_handlers[Priority.AFTER].get(name, []): try: listener(*args, **kwargs) - except Exception: - pass + except Exception as e: + self.log.warning('AFTER {} event handler `{}` raised {}: {}'.format( + name, + listener.callback.__name__, + e.__class__.__name__, + e, + )) # Next enqueue all sequential handlers. This just puts stuff into a queue # without blocking, so we don't have to worry too much diff --git a/disco/util/functional.py b/disco/util/functional.py index c8f084d..3efa4af 100644 --- a/disco/util/functional.py +++ b/disco/util/functional.py @@ -1,4 +1,4 @@ -from six.moves import range +import six NO_MORE_SENTINEL = object() @@ -14,7 +14,7 @@ def take(seq, count): count : int The number of elements to take. """ - for _ in range(count): + for _ in six.moves.range(count): i = next(seq, NO_MORE_SENTINEL) if i is NO_MORE_SENTINEL: return @@ -32,7 +32,7 @@ def chunks(obj, size): size : int Size of chunks to split list into. """ - for i in range(0, len(obj), size): + for i in six.moves.range(0, len(obj), size): yield obj[i:i + size] @@ -66,3 +66,12 @@ def simple_cached_property(method): delattr(inst, key) return property(_getattr, _setattr, _delattr) + + +def optional(**kwargs): + """ + Takes a set of keyword arguments, creating a dictionary with only the non- + null values. + :returns: dict + """ + return {k: v for k, v in six.iteritems(kwargs) if v is not None}