Browse Source

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
staging/v1.0.0
Luke 5 years ago
committed by Andrei Zbikowski
parent
commit
8dbe0421f6
  1. 12
      disco/api/client.py
  2. 15
      disco/gateway/client.py
  3. 6
      disco/gateway/events.py
  4. 14
      disco/state.py
  5. 7
      disco/types/guild.py
  6. 23
      disco/util/emitter.py
  7. 15
      disco/util/functional.py

12
disco/api/client.py

@ -1,4 +1,3 @@
import six
import json import json
import warnings import warnings
@ -7,6 +6,7 @@ from gevent.local import local
from six.moves.urllib.parse import quote from six.moves.urllib.parse import quote
from disco.api.http import Routes, HTTPClient, to_bytes from disco.api.http import Routes, HTTPClient, to_bytes
from disco.util.functional import optional
from disco.util.logging import LoggingClass from disco.util.logging import LoggingClass
from disco.util.sanitize import S from disco.util.sanitize import S
from disco.types.user import User from disco.types.user import User
@ -22,16 +22,6 @@ from disco.types.voice import VoiceRegion
from disco.types.webhook import Webhook 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): def _reason_header(value):
return optional(**{'X-Audit-Log-Reason': quote(to_bytes(value)) if value else None}) return optional(**{'X-Audit-Log-Reason': quote(to_bytes(value)) if value else None})

15
disco/gateway/client.py

@ -273,7 +273,7 @@ class GatewayClient(LoggingClass):
gevent.spawn(self.connect_and_run) gevent.spawn(self.connect_and_run)
self.ws_event.wait() 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 Request a batch of Guild members from Discord. Generally this function
can be called when initially loading Guilds to fill the local member state. 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, { self.send(OPCode.REQUEST_GUILD_MEMBERS, {
# This is simply unfortunate naming on the part of Discord... # This is simply unfortunate naming on the part of Discord...
'guild_id': guild_id_or_ids, 'guild_id': guild_id_or_ids,
'limit': limit,
'presences': presences,
'query': query or '', '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, 'limit': limit,
'presences': presences,
# This is simply even more unfortunate naming from Discord...
'user_ids': user_id_or_ids,
}) })

6
disco/gateway/events.py

@ -338,9 +338,15 @@ class GuildMembersChunk(GatewayEvent):
The ID of the guild this member chunk is for. The ID of the guild this member chunk is for.
members : list[:class:`disco.types.guild.GuildMember`] members : list[:class:`disco.types.guild.GuildMember`]
The chunk of members. 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) guild_id = Field(snowflake)
members = ListField(GuildMember) members = ListField(GuildMember)
not_found = ListField(snowflake)
presences = ListField(Presence)
@property @property
def guild(self): def guild(self):

14
disco/state.py

@ -229,6 +229,11 @@ class State(object):
elif event.channel.is_dm: elif event.channel.is_dm:
self.dms[event.channel.id] = event.channel self.dms[event.channel.id] = event.channel
self.channels[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): def on_channel_update(self, event):
if event.channel.id in self.channels: if event.channel.id in self.channels:
@ -325,6 +330,15 @@ class State(object):
else: else:
member.user = self.users[member.id] 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): def on_guild_role_create(self, event):
if event.guild_id not in self.guilds: if event.guild_id not in self.guilds:
return return

7
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) return self.client.api.guilds_roles_modify(self.id, to_snowflake(role), **kwargs)
def request_guild_members(self, query=None, limit=0): def request_guild_members(self, query=None, limit=0, presences=False):
self.client.gw.request_guild_members(self.id, query, limit) 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): def sync(self):
warnings.warn( warnings.warn(

23
disco/util/emitter.py

@ -5,6 +5,9 @@ from gevent.event import AsyncResult
from gevent.queue import Queue, Full from gevent.queue import Queue, Full
from disco.util.logging import LoggingClass
class Priority(object): class Priority(object):
# BEFORE is the most dangerous priority level. Every event that flows through # BEFORE is the most dangerous priority level. Every event that flows through
# the given emitter instance will be dispatched _sequentially_ to all BEFORE # the given emitter instance will be dispatched _sequentially_ to all BEFORE
@ -106,7 +109,7 @@ class EmitterSubscription(object):
self.detach(emitter) self.detach(emitter)
class Emitter(object): class Emitter(LoggingClass):
def __init__(self): def __init__(self):
self.event_handlers = { self.event_handlers = {
k: defaultdict(list) for k in Priority.ALL 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, []): for listener in self.event_handlers[Priority.BEFORE].get(name, []):
try: try:
listener(*args, **kwargs) listener(*args, **kwargs)
except Exception: except Exception as e:
pass self.log.warning('BEFORE {} event handler `{}` raised {}: {}'.format(
name,
listener.callback.__name__,
e.__class__.__name__,
e,
))
# Next execute all AFTER handlers sequentially # Next execute all AFTER handlers sequentially
for listener in self.event_handlers[Priority.AFTER].get(name, []): for listener in self.event_handlers[Priority.AFTER].get(name, []):
try: try:
listener(*args, **kwargs) listener(*args, **kwargs)
except Exception: except Exception as e:
pass 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 # Next enqueue all sequential handlers. This just puts stuff into a queue
# without blocking, so we don't have to worry too much # without blocking, so we don't have to worry too much

15
disco/util/functional.py

@ -1,4 +1,4 @@
from six.moves import range import six
NO_MORE_SENTINEL = object() NO_MORE_SENTINEL = object()
@ -14,7 +14,7 @@ def take(seq, count):
count : int count : int
The number of elements to take. The number of elements to take.
""" """
for _ in range(count): for _ in six.moves.range(count):
i = next(seq, NO_MORE_SENTINEL) i = next(seq, NO_MORE_SENTINEL)
if i is NO_MORE_SENTINEL: if i is NO_MORE_SENTINEL:
return return
@ -32,7 +32,7 @@ def chunks(obj, size):
size : int size : int
Size of chunks to split list into. 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] yield obj[i:i + size]
@ -66,3 +66,12 @@ def simple_cached_property(method):
delattr(inst, key) delattr(inst, key)
return property(_getattr, _setattr, _delattr) 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}

Loading…
Cancel
Save