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.
This commit is contained in:
		@@ -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|
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user