From 17b7ea35a959b86ed6aa06d0d5714e523ef0e283 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Wed, 11 Jun 2025 02:06:19 -0600 Subject: [PATCH] feat: Implement guild.fetch_members --- disagreement/gateway.py | 44 +++++++++++++++++++++++++++++++++++++++++ disagreement/models.py | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/disagreement/gateway.py b/disagreement/gateway.py index 7cd53cc..ff132d7 100644 --- a/disagreement/gateway.py +++ b/disagreement/gateway.py @@ -79,6 +79,8 @@ class GatewayClient: self._buffer = bytearray() self._inflator = zlib.decompressobj() + self._member_chunk_requests: Dict[str, asyncio.Future] = {} + async def _reconnect(self) -> None: """Attempts to reconnect using exponential backoff with jitter.""" delay = 1.0 @@ -237,6 +239,32 @@ class GatewayClient: } await self._send_json(payload) + async def request_guild_members( + self, + guild_id: str, + query: str = "", + limit: int = 0, + presences: bool = False, + user_ids: Optional[list[str]] = None, + nonce: Optional[str] = None, + ): + """Sends the request guild members payload to the Gateway.""" + payload = { + "op": GatewayOpcode.REQUEST_GUILD_MEMBERS, + "d": { + "guild_id": guild_id, + "query": query, + "limit": limit, + "presences": presences, + }, + } + if user_ids: + payload["d"]["user_ids"] = user_ids + if nonce: + payload["d"]["nonce"] = nonce + + await self._send_json(payload) + async def _handle_dispatch(self, data: Dict[str, Any]): """Handles DISPATCH events (actual Discord events).""" event_name = data.get("t") @@ -313,6 +341,22 @@ class GatewayClient: ) await self._dispatcher.dispatch(event_name, raw_event_d_payload) + elif event_name == "GUILD_MEMBERS_CHUNK": + if isinstance(raw_event_d_payload, dict): + nonce = raw_event_d_payload.get("nonce") + if nonce and nonce in self._member_chunk_requests: + future = self._member_chunk_requests[nonce] + if not future.done(): + # Append members to a temporary list stored on the future object + if not hasattr(future, "_members"): + future._members = [] # type: ignore + future._members.extend(raw_event_d_payload.get("members", [])) # type: ignore + + # If this is the last chunk, resolve the future + if raw_event_d_payload.get("chunk_index") == raw_event_d_payload.get("chunk_count", 1) - 1: + future.set_result(future._members) # type: ignore + del self._member_chunk_requests[nonce] + elif event_name == "INTERACTION_CREATE": # print(f"GATEWAY RECV INTERACTION_CREATE: {raw_event_d_payload}") if isinstance(raw_event_d_payload, dict): diff --git a/disagreement/models.py b/disagreement/models.py index 0666926..37c9baf 100644 --- a/disagreement/models.py +++ b/disagreement/models.py @@ -1123,6 +1123,49 @@ class Guild: def __repr__(self) -> str: return f"" + async def fetch_members(self, *, limit: Optional[int] = None) -> List["Member"]: + """|coro| + + Fetches all members for this guild. + + This requires the ``GUILD_MEMBERS`` intent. + + Parameters + ---------- + limit: Optional[int] + The maximum number of members to fetch. If ``None``, all members + are fetched. + + Returns + ------- + List[Member] + A list of all members in the guild. + + Raises + ------ + DisagreementException + The gateway is not available to make the request. + asyncio.TimeoutError + The request timed out. + """ + if not self._client._gateway: + raise DisagreementException("Gateway not available for member fetching.") + + nonce = str(asyncio.get_running_loop().time()) + future = self._client._gateway._loop.create_future() + self._client._gateway._member_chunk_requests[nonce] = future + + try: + await self._client._gateway.request_guild_members( + self.id, limit=limit or 0, nonce=nonce + ) + member_data = await asyncio.wait_for(future, timeout=60.0) + return [Member(m, self._client) for m in member_data] + except asyncio.TimeoutError: + if nonce in self._client._gateway._member_chunk_requests: + del self._client._gateway._member_chunk_requests[nonce] + raise + class Channel: """Base class for Discord channels."""