From 6a6ec5f3340055d78abf6c647a977f2f9a134e83 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 28 Oct 2019 22:46:42 +0000 Subject: [PATCH] request_guild_members update, logging on emitter event exceptions and improved state caching. --- disco/api/client.py | 12 +----------- disco/gateway/client.py | 16 ++++++++++++---- disco/state.py | 22 ++++++++++++++++++++++ disco/types/guild.py | 4 ++-- disco/util/emitter.py | 23 ++++++++++++++++++----- disco/util/functional.py | 15 ++++++++++++--- 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/disco/api/client.py b/disco/api/client.py index e1ca1f7..02df534 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 @@ -18,16 +18,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 2876f48..ce50f62 100644 --- a/disco/gateway/client.py +++ b/disco/gateway/client.py @@ -9,6 +9,7 @@ from websocket import ABNF from disco.gateway.packets import OPCode, RECV, SEND from disco.gateway.events import GatewayEvent from disco.gateway.encoding import ENCODERS +from disco.util.functional import optional from disco.util.websocket import Websocket from disco.util.logging import LoggingClass from disco.util.limiter import SimpleLimiter @@ -266,14 +267,21 @@ 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, user_id_or_ids=None, presences=None): """ Request a batch of Guild members from Discord. Generally this function can be called when initially loading Guilds to fill the local member state. + When calling this function, if user_id_or_ids is passed then query will be ignored. """ - self.send(OPCode.REQUEST_GUILD_MEMBERS, { + payload = { # This is simply unfortunate naming on the part of Discord... 'guild_id': guild_id_or_ids, - 'query': query or '', 'limit': limit, - }) + } + payload.update(optional( + # This is simply Discord sticking to an unfortunate naming scheme... + user_ids=user_id_or_ids, + query=query or '' if user_id_or_ids is None else None, + presences=presences, + )) + self.send(OPCode.REQUEST_GUILD_MEMBERS, payload) diff --git a/disco/state.py b/disco/state.py index 28a54ff..2ff3aab 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: @@ -285,6 +290,11 @@ class State(object): if event.member.guild_id not in self.guilds: return + if (self.guilds[event.member.guild_id].member_count is not UNSET and + # Avoid adding duplicate events to member_count. + event.member.id not in self.guilds[event.member.guild_id].members): + self.guilds[event.member.guild_id].member_count += 1 + self.guilds[event.member.guild_id].members[event.member.id] = event.member def on_guild_member_update(self, event): @@ -303,6 +313,9 @@ class State(object): if event.user.id not in self.guilds[event.guild_id].members: return + if self.guilds[event.guild_id].member_count is not UNSET: + self.guilds[event.guild_id].member_count -= 1 + del self.guilds[event.guild_id].members[event.user.id] def on_guild_members_chunk(self, event): @@ -319,6 +332,15 @@ class State(object): else: member.user = self.users[member.id] + if not event.presences: + return + + for presence in event.presences: + if presence.user.id not in self.users: + self.users[presence.user.id] = presence.user + + self.users[presence.user.id].presence = presence + 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 e7c16a7..57240e8 100644 --- a/disco/types/guild.py +++ b/disco/types/guild.py @@ -450,8 +450,8 @@ 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, user_id_or_ids=None, presences=None): + self.client.gw.request_guild_members(self.id, query, limit, user_id_or_ids, presences) def sync(self): warnings.warn( diff --git a/disco/util/emitter.py b/disco/util/emitter.py index 35e7bb6..9446c0e 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__, + str(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__, + str(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 0a06048..0c49bf6 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}