Browse Source

Offline members are now added by default automatically.

This commit adds support for GUILD_MEMBERS_CHUNK which had to be done
due to forced large_threshold requirements in the library.
pull/105/head
Rapptz 9 years ago
parent
commit
4768d950c5
  1. 87
      discord/client.py
  2. 8
      discord/server.py
  3. 60
      discord/state.py

87
discord/client.py

@ -51,7 +51,7 @@ import logging, traceback
import sys, time, re, json import sys, time, re, json
import tempfile, os, hashlib import tempfile, os, hashlib
import itertools import itertools
import zlib import zlib, math
from random import randint as random_integer from random import randint as random_integer
PY35 = sys.version_info >= (3, 5) PY35 = sys.version_info >= (3, 5)
@ -81,6 +81,10 @@ class Client:
Indicates if :meth:`login` should cache the authentication tokens. Defaults Indicates if :meth:`login` should cache the authentication tokens. Defaults
to ``True``. The method in which the cache is written is done by writing to to ``True``. The method in which the cache is written is done by writing to
disk to a temporary directory. disk to a temporary directory.
request_offline : Optional[bool]
Indicates if the client should request the offline members of every server.
If this is False, then member lists will not store offline members if the
number of members in the server is greater than 250. Defaults to ``True``.
Attributes Attributes
----------- -----------
@ -117,12 +121,13 @@ class Client:
self.loop = asyncio.get_event_loop() if loop is None else loop self.loop = asyncio.get_event_loop() if loop is None else loop
self._listeners = [] self._listeners = []
self.cache_auth = options.get('cache_auth', True) self.cache_auth = options.get('cache_auth', True)
self.request_offline = options.get('request_offline', True)
max_messages = options.get('max_messages') max_messages = options.get('max_messages')
if max_messages is None or max_messages < 100: if max_messages is None or max_messages < 100:
max_messages = 5000 max_messages = 5000
self.connection = ConnectionState(self.dispatch, max_messages) self.connection = ConnectionState(self.dispatch, max_messages, loop=self.loop)
# Blame React for this # Blame React for this
user_agent = 'DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}' user_agent = 'DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}'
@ -143,6 +148,25 @@ class Client:
# internals # internals
def _get_all_chunks(self):
# a chunk has a maximum of 1000 members.
# we need to find out how many futures we're actually waiting for
large_servers = filter(lambda s: s.large, self.servers)
futures = []
for server in large_servers:
chunks_needed = math.ceil(server._member_count / 1000)
for chunk in range(chunks_needed):
futures.append(self.connection.receive_chunk(server.id))
return futures
@asyncio.coroutine
def _fill_offline(self):
yield from self.request_offline_members(filter(lambda s: s.large, self.servers))
chunks = self._get_all_chunks()
yield from asyncio.wait(chunks)
self.dispatch('ready')
def _get_cache_filename(self, email): def _get_cache_filename(self, email):
filename = hashlib.md5(email.encode('utf-8')).hexdigest() filename = hashlib.md5(email.encode('utf-8')).hexdigest()
return os.path.join(tempfile.gettempdir(), 'discord_py', filename) return os.path.join(tempfile.gettempdir(), 'discord_py', filename)
@ -335,12 +359,13 @@ class Client:
return return
event = msg.get('t') event = msg.get('t')
is_ready = event == 'READY'
if event == 'READY': if is_ready:
self.connection.clear() self.connection.clear()
self.session_id = data['session_id'] self.session_id = data['session_id']
if event == 'READY' or event == 'RESUMED': if is_ready or event == 'RESUMED':
interval = data['heartbeat_interval'] / 1000.0 interval = data['heartbeat_interval'] / 1000.0
self.keep_alive = utils.create_task(self.keep_alive_handler(interval), loop=self.loop) self.keep_alive = utils.create_task(self.keep_alive_handler(interval), loop=self.loop)
@ -362,10 +387,19 @@ class Client:
return return
parser = 'parse_' + event.lower() parser = 'parse_' + event.lower()
if hasattr(self.connection, parser):
getattr(self.connection, parser)(data) try:
func = getattr(self.connection, parser)
except AttributeError:
log.info('Unhandled event {}'.format(event))
else: else:
log.info("Unhandled event {}".format(event)) func(data)
if is_ready:
if self.request_offline:
utils.create_task(self._fill_offline(), loop=self.loop)
else:
self.dispatch('ready')
@asyncio.coroutine @asyncio.coroutine
def _make_websocket(self, initial=True): def _make_websocket(self, initial=True):
@ -389,6 +423,7 @@ class Client:
'$referring_domain': '' '$referring_domain': ''
}, },
'compress': True, 'compress': True,
'large_threshold': 250,
'v': 3 'v': 3
} }
} }
@ -1218,6 +1253,44 @@ class Client:
# Member management # Member management
@asyncio.coroutine
def request_offline_members(self, server):
"""|coro|
Requests previously offline members from the server to be filled up
into the :attr:`Server.members` cache. If the client was initialised
with ``request_offline`` as ``True`` then calling this function would
not do anything.
When the client logs on and connects to the websocket, Discord does
not provide the library with offline members if the number of members
in the server is larger than 250. You can check if a server is large
if :attr:`Server.large` is ``True``.
Parameters
-----------
server : :class:`Server` or iterable
The server to request offline members for. If this parameter is a
iterable then it is interpreted as an iterator of servers to
request offline members for.
"""
if hasattr(server, 'id'):
guild_id = server.id
else:
guild_id = [s.id for s in server]
payload = {
'op': 8,
'd': {
'guild_id': guild_id,
'query': '',
'limit': 0
}
}
yield from self._send_ws(utils.to_json(payload))
@asyncio.coroutine @asyncio.coroutine
def kick(self, member): def kick(self, member):
"""|coro| """|coro|

8
discord/server.py

@ -84,9 +84,10 @@ class Server(Hashable):
Check the :func:`on_server_unavailable` and :func:`on_server_available` events. Check the :func:`on_server_unavailable` and :func:`on_server_available` events.
""" """
__slots__ = [ 'afk_timeout', 'afk_channel', '_members', '_channels', 'icon', __slots__ = ['afk_timeout', 'afk_channel', '_members', '_channels', 'icon',
'name', 'id', 'owner', 'unavailable', 'name', 'me', 'region', 'name', 'id', 'owner', 'unavailable', 'name', 'me', 'region',
'_default_role', '_default_channel', 'roles', '_member_count'] '_default_role', '_default_channel', 'roles', '_member_count',
'large' ]
def __init__(self, **kwargs): def __init__(self, **kwargs):
self._channels = {} self._channels = {}
@ -139,6 +140,7 @@ class Server(Hashable):
# according to Stan, this is always available even if the guild is unavailable # according to Stan, this is always available even if the guild is unavailable
self._member_count = guild['member_count'] self._member_count = guild['member_count']
self.name = guild.get('name') self.name = guild.get('name')
self.large = guild.get('large', self._member_count > 250)
self.region = guild.get('region') self.region = guild.get('region')
try: try:
self.region = ServerRegion(self.region) self.region = ServerRegion(self.region)

60
discord/state.py

@ -34,14 +34,26 @@ from .role import Role
from . import utils from . import utils
from .enums import Status from .enums import Status
from collections import deque
from collections import deque, namedtuple
import copy import copy
import datetime import datetime
import asyncio
import enum
import logging
class ListenerType(enum.Enum):
chunk = 0
Listener = namedtuple('Listener', ('type', 'future', 'predicate'))
log = logging.getLogger(__name__)
class ConnectionState: class ConnectionState:
def __init__(self, dispatch, max_messages): def __init__(self, dispatch, max_messages, *, loop):
self.loop = loop
self.max_messages = max_messages self.max_messages = max_messages
self.dispatch = dispatch self.dispatch = dispatch
self._listeners = []
self.clear() self.clear()
def clear(self): def clear(self):
@ -52,6 +64,30 @@ class ConnectionState:
self._private_channels_by_user = {} self._private_channels_by_user = {}
self.messages = deque(maxlen=self.max_messages) self.messages = deque(maxlen=self.max_messages)
def process_listeners(self, listener_type, argument, result):
removed = []
for i, listener in enumerate(self._listeners):
if listener.type != listener_type:
continue
future = listener.future
if future.cancelled():
removed.append(i)
continue
try:
passed = listener.predicate(argument)
except Exception as e:
future.set_exception(e)
removed.append(i)
else:
if passed:
future.set_result(result)
removed.append(i)
for index in reversed(removed):
del self._listeners[index]
@property @property
def servers(self): def servers(self):
return self._servers.values() return self._servers.values()
@ -103,9 +139,6 @@ class ConnectionState:
self._add_private_channel(PrivateChannel(id=pm['id'], self._add_private_channel(PrivateChannel(id=pm['id'],
user=User(**pm['recipient']))) user=User(**pm['recipient'])))
# we're all ready
self.dispatch('ready')
def parse_message_create(self, data): def parse_message_create(self, data):
channel = self.get_channel(data.get('channel_id')) channel = self.get_channel(data.get('channel_id'))
message = Message(channel=channel, **data) message = Message(channel=channel, **data)
@ -213,7 +246,7 @@ class ConnectionState:
def parse_guild_member_add(self, data): def parse_guild_member_add(self, data):
server = self._get_server(data.get('guild_id')) server = self._get_server(data.get('guild_id'))
self._add_member(server, data) member = self._add_member(server, data)
server._member_count += 1 server._member_count += 1
self.dispatch('member_join', member) self.dispatch('member_join', member)
@ -345,6 +378,15 @@ class ConnectionState:
role._update(**data['role']) role._update(**data['role'])
self.dispatch('server_role_update', old_role, role) self.dispatch('server_role_update', old_role, role)
def parse_guild_members_chunk(self, data):
server = self._get_server(data.get('guild_id'))
members = data.get('members', [])
for member in members:
self._add_member(server, member)
log.info('processed a chunk for {} members.'.format(len(members)))
self.process_listeners(ListenerType.chunk, server, len(members))
def parse_voice_state_update(self, data): def parse_voice_state_update(self, data):
server = self._get_server(data.get('guild_id')) server = self._get_server(data.get('guild_id'))
if server is not None: if server is not None:
@ -381,3 +423,9 @@ class ConnectionState:
pm = self._get_private_channel(id) pm = self._get_private_channel(id)
if pm is not None: if pm is not None:
return pm return pm
def receive_chunk(self, guild_id):
future = asyncio.Future(loop=self.loop)
listener = Listener(ListenerType.chunk, future, lambda s: s.id == guild_id)
self._listeners.append(listener)
return future

Loading…
Cancel
Save