Browse Source

Implement Search Recent Members OP

pull/10109/head
dolfies 2 years ago
parent
commit
8e8d0ff12a
  1. 4
      discord/experiment.py
  2. 19
      discord/gateway.py
  3. 56
      discord/guild.py
  4. 108
      discord/state.py
  5. 11
      discord/user.py

4
discord/experiment.py

@ -702,13 +702,13 @@ class UserExperiment:
Returns the experiment's hash. Returns the experiment's hash.
.. versionadded:: 2.1
.. note:: .. note::
In contrast to the wide range of data provided for guild experiments, In contrast to the wide range of data provided for guild experiments,
user experiments do not reveal detailed rollout information, providing only the assigned bucket. user experiments do not reveal detailed rollout information, providing only the assigned bucket.
.. versionadded:: 2.1
Attributes Attributes
---------- ----------
hash: :class:`int` hash: :class:`int`

19
discord/gateway.py

@ -311,6 +311,7 @@ class DiscordWebSocket:
CALL_CONNECT = 13 CALL_CONNECT = 13
GUILD_SUBSCRIBE = 14 GUILD_SUBSCRIBE = 14
REQUEST_COMMANDS = 24 REQUEST_COMMANDS = 24
SEARCH_RECENT_MEMBERS = 35
# fmt: on # fmt: on
def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None: def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None:
@ -325,7 +326,7 @@ class DiscordWebSocket:
self._keep_alive: Optional[KeepAliveHandler] = None self._keep_alive: Optional[KeepAliveHandler] = None
self.thread_id: int = threading.get_ident() self.thread_id: int = threading.get_ident()
# ws related stuff # WS related stuff
self.session_id: Optional[str] = None self.session_id: Optional[str] = None
self.sequence: Optional[int] = None self.sequence: Optional[int] = None
self._zlib: zlib._Decompress = zlib.decompressobj() self._zlib: zlib._Decompress = zlib.decompressobj()
@ -855,6 +856,22 @@ class DiscordWebSocket:
await self.send_as_json(payload) await self.send_as_json(payload)
async def search_recent_members(
self, guild_id: Snowflake, query: str = '', *, after: Optional[Snowflake] = None, nonce: Optional[str] = None
) -> None:
payload = {
'op': self.SEARCH_RECENT_MEMBERS,
'd': {
'guild_id': str(guild_id),
'query': query,
'continuation_token': str(after) if after else None,
},
}
if nonce is not None:
payload['d']['nonce'] = nonce
await self.send_as_json(payload)
async def close(self, code: int = 4000) -> None: async def close(self, code: int = 4000) -> None:
if self._keep_alive: if self._keep_alive:
self._keep_alive.stop() self._keep_alive.stop()

56
discord/guild.py

@ -4799,6 +4799,7 @@ class Guild(Hashable):
cache: :class:`bool` cache: :class:`bool`
Whether to cache the members internally. This makes operations Whether to cache the members internally. This makes operations
such as :meth:`get_member` work for those that matched. such as :meth:`get_member` work for those that matched.
The cache will not be kept updated unless ``subscribe`` is set to ``True``.
user_ids: Optional[List[:class:`int`]] user_ids: Optional[List[:class:`int`]]
List of user IDs to search for. If the user ID is not in the guild then it won't be returned. List of user IDs to search for. If the user ID is not in the guild then it won't be returned.
@ -4837,6 +4838,61 @@ class Guild(Hashable):
await self._state.ws.request_lazy_guild(self.id, members=ids) await self._state.ws.request_lazy_guild(self.id, members=ids)
return members return members
async def query_recent_members(
self,
query: Optional[str] = None,
*,
limit: int = 1000,
cache: bool = True,
subscribe: bool = False,
) -> List[Member]:
"""|coro|
Request the most recent 10,000 joined members of this guild.
This is a websocket operation.
.. note::
This operation does not return presences.
.. versionadded:: 2.1
Parameters
-----------
query: Optional[:class:`str`]
The string that the username or nickname should start with, if any.
limit: :class:`int`
The maximum number of members to send back. This must be
a number between 1 and 10,000.
cache: :class:`bool`
Whether to cache the members internally. This makes operations
such as :meth:`get_member` work for those that matched.
The cache will not be kept updated unless ``subscribe`` is set to ``True``.
subscribe: :class:`bool`
Whether to subscribe to the resulting members. This will keep their info and presence updated.
This requires another request, and defaults to ``False``.
Raises
-------
asyncio.TimeoutError
The query timed out waiting for the members.
TypeError
Invalid parameters were passed to the function.
Returns
--------
List[:class:`Member`]
The list of members that have matched the query.
"""
limit = min(10000, limit or 1)
members = await self._state.search_recent_members(self, query or '', limit, cache)
if subscribe:
ids: List[_Snowflake] = [str(m.id) for m in members]
for i in range(0, len(ids), 750):
subs = ids[i : i + 750]
await self._state.ws.request_lazy_guild(self.id, members=subs)
return members
async def change_voice_state( async def change_voice_state(
self, self,
*, *,

108
discord/state.py

@ -140,24 +140,55 @@ _log = logging.getLogger(__name__)
class ChunkRequest: class ChunkRequest:
__slots__ = (
'guild_id',
'resolver',
'loop',
'limit',
'remaining',
'cache',
'oneshot',
'nonce',
'buffer',
'last_buffer',
'waiters',
)
def __init__( def __init__(
self, self,
guild_id: int, guild_id: int,
loop: asyncio.AbstractEventLoop, loop: asyncio.AbstractEventLoop,
resolver: Callable[[int], Any], resolver: Callable[[int], Any],
*, *,
limit: Optional[int] = None,
cache: bool = True, cache: bool = True,
oneshot: bool = True,
) -> None: ) -> None:
self.guild_id: int = guild_id self.guild_id: int = guild_id
self.resolver: Callable[[int], Any] = resolver self.resolver: Callable[[int], Any] = resolver
self.loop: asyncio.AbstractEventLoop = loop self.loop: asyncio.AbstractEventLoop = loop
self.limit: Optional[int] = limit
self.remaining: int = limit or 0
self.cache: bool = cache self.cache: bool = cache
self.oneshot: bool = oneshot
self.nonce: str = str(utils.time_snowflake(utils.utcnow())) self.nonce: str = str(utils.time_snowflake(utils.utcnow()))
self.buffer: List[Member] = [] self.buffer: List[Member] = []
self.last_buffer: Optional[List[Member]] = None
self.waiters: List[asyncio.Future[List[Member]]] = [] self.waiters: List[asyncio.Future[List[Member]]] = []
def add_members(self, members: List[Member]) -> None: def add_members(self, members: List[Member]) -> None:
unique_members = set(members)
if self.limit is not None:
if self.remaining <= 0:
return
members = list(unique_members)[: self.remaining]
self.remaining -= len(unique_members)
else:
members = list(unique_members)
self.buffer.extend(members) self.buffer.extend(members)
if self.cache: if self.cache:
guild = self.resolver(self.guild_id) guild = self.resolver(self.guild_id)
if guild is None: if guild is None:
@ -166,6 +197,9 @@ class ChunkRequest:
for member in members: for member in members:
guild._add_member(member) guild._add_member(member)
if not self.oneshot:
self.last_buffer = members
async def wait(self) -> List[Member]: async def wait(self) -> List[Member]:
future = self.loop.create_future() future = self.loop.create_future()
self.waiters.append(future) self.waiters.append(future)
@ -180,12 +214,28 @@ class ChunkRequest:
return future return future
def done(self) -> None: def done(self) -> None:
result = self.buffer if self.oneshot else self.last_buffer or self.buffer
for future in self.waiters: for future in self.waiters:
if not future.done(): if not future.done():
future.set_result(self.buffer) future.set_result(result)
class MemberSidebar: class MemberSidebar:
__slots__ = (
'guild',
'channels',
'chunk',
'delay',
'cache',
'loop',
'safe_override',
'ranges',
'subscribing',
'buffer',
'exception',
'waiters',
)
def __init__( def __init__(
self, self,
guild: Guild, guild: Guild,
@ -301,6 +351,7 @@ class MemberSidebar:
return list(ret) return list(ret)
def add_members(self, members: List[Member]) -> None: def add_members(self, members: List[Member]) -> None:
members = list(set(members))
self.buffer.extend(members) self.buffer.extend(members)
if self.cache: if self.cache:
guild = self.guild guild = self.guild
@ -653,6 +704,7 @@ class ConnectionState:
request.add_members(members) request.add_members(members)
if complete: if complete:
request.done() request.done()
if request.oneshot:
removed.append(key) removed.append(key)
for key in removed: for key in removed:
@ -905,9 +957,18 @@ class ConnectionState:
return self.ws.request_lazy_guild(guild_id, typing=typing, activities=activities, threads=threads) return self.ws.request_lazy_guild(guild_id, typing=typing, activities=activities, threads=threads)
def chunker( def chunker(
self, guild_id: int, query: str = '', limit: int = 0, presences: bool = True, *, nonce: Optional[str] = None self,
guild_id: int,
query: Optional[str] = '',
limit: int = 0,
presences: bool = True,
*,
user_ids: Optional[List[Snowflake]] = None,
nonce: Optional[str] = None,
): ):
return self.ws.request_chunks([guild_id], query=query, limit=limit, presences=presences, nonce=nonce) return self.ws.request_chunks(
[guild_id], query=query, limit=limit, presences=presences, user_ids=user_ids, nonce=nonce
)
async def query_members( async def query_members(
self, self,
@ -923,14 +984,51 @@ class ConnectionState:
self._chunk_requests[request.nonce] = request self._chunk_requests[request.nonce] = request
try: try:
await self.ws.request_chunks( await self.chunker(
[guild_id], query=query, limit=limit, user_ids=user_ids, presences=presences, nonce=request.nonce guild_id, query=query, limit=limit, presences=presences, user_ids=user_ids, nonce=request.nonce
) )
return await asyncio.wait_for(request.wait(), timeout=30.0) return await asyncio.wait_for(request.wait(), timeout=30.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_log.warning('Timed out waiting for chunks with query %r and limit %d for guild_id %d.', query, limit, guild_id) _log.warning('Timed out waiting for chunks with query %r and limit %d for guild_id %d.', query, limit, guild_id)
raise raise
async def search_recent_members(
self,
guild: Guild,
query: str = '',
limit: Optional[int] = None,
cache: bool = False,
) -> List[Member]:
guild_id = guild.id
request = ChunkRequest(guild.id, self.loop, self._get_guild, limit=limit, cache=cache, oneshot=False)
self._chunk_requests[request.nonce] = request
# Unlike query members, this OP is paginated
old_continuation_token = None
continuation_token = None
while True:
try:
await self.ws.search_recent_members(guild_id, query=query, nonce=request.nonce, after=continuation_token)
returned = await asyncio.wait_for(request.wait(), timeout=30.0)
except asyncio.TimeoutError:
_log.warning(
'Timed out waiting for search chunks with query %r and limit %d for guild_id %d.', query, limit, guild_id
)
raise
if (limit is not None and request.remaining < 1) or len(returned) < 1:
break
# Sort the members by joined_at timestamp and grab the oldest one
request.buffer.sort(key=lambda m: m.joined_at or utils.utcnow())
old_continuation_token = continuation_token
continuation_token = request.buffer[0].id
if continuation_token == old_continuation_token:
break
self._chunk_requests.pop(request.nonce, None)
return list(set(request.buffer))
async def _delay_ready(self) -> None: async def _delay_ready(self) -> None:
try: try:
states = [] states = []

11
discord/user.py

@ -1024,11 +1024,18 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable):
user['discriminator'], user['discriminator'],
user.get('public_flags', 0), user.get('public_flags', 0),
user.get('avatar_decoration'), user.get('avatar_decoration'),
user.get('global_name') user.get('global_name'),
) )
if original != modified: if original != modified:
to_return = User._copy(self) to_return = User._copy(self)
self.name, self._avatar, self.discriminator, self._public_flags, self._avatar_decoration, self.global_name = modified (
self.name,
self._avatar,
self.discriminator,
self._public_flags,
self._avatar_decoration,
self.global_name,
) = modified
# Signal to dispatch user_update # Signal to dispatch user_update
return to_return, self return to_return, self

Loading…
Cancel
Save