Skip to content

Commit 51d43a1

Browse files
authored
Rework member chunking and events (#662)
1 parent 553cfa2 commit 51d43a1

19 files changed

+1239
-514
lines changed

discord/abc.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,21 @@ def __str__(self) -> str:
622622
def _sorting_bucket(self) -> int:
623623
raise NotImplementedError
624624

625+
@property
626+
def member_list_id(self) -> Union[str, Literal["everyone"]]:
627+
if self.permissions_for(self.guild.default_role).read_messages:
628+
return "everyone"
629+
630+
overwrites = []
631+
for overwrite in self._overwrites:
632+
allow, deny = Permissions(overwrite.allow), Permissions(overwrite.deny)
633+
if allow.read_messages:
634+
overwrites.append(f"allow:{overwrite.id}")
635+
elif deny.read_messages:
636+
overwrites.append(f"deny:{overwrite.id}")
637+
638+
return str(utils.murmurhash32(",".join(sorted(overwrites)), signed=False))
639+
625640
def _update(self, guild: Guild, data: Dict[str, Any]) -> None:
626641
raise NotImplementedError
627642

discord/channel.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3458,7 +3458,6 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr
34583458
'_requested_at',
34593459
'_spam',
34603460
'_state',
3461-
'_accessed',
34623461
)
34633462

34643463
def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload):
@@ -3467,7 +3466,6 @@ def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPay
34673466
self.me: ClientUser = me
34683467
self.id: int = int(data['id'])
34693468
self._update(data)
3470-
self._accessed: bool = False
34713469

34723470
def _update(self, data: DMChannelPayload) -> None:
34733471
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
@@ -3486,9 +3484,6 @@ def _add_call(self, **kwargs) -> PrivateCall:
34863484
return PrivateCall(**kwargs)
34873485

34883486
async def _get_channel(self) -> Self:
3489-
if not self._accessed:
3490-
await self._state.call_connect(self.id)
3491-
self._accessed = True
34923487
return self
34933488

34943489
async def _initial_ring(self) -> None:
@@ -3912,15 +3907,13 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc
39123907
'name',
39133908
'me',
39143909
'_state',
3915-
'_accessed',
39163910
)
39173911

39183912
def __init__(self, *, me: ClientUser, state: ConnectionState, data: GroupChannelPayload):
39193913
self._state: ConnectionState = state
39203914
self.id: int = int(data['id'])
39213915
self.me: ClientUser = me
39223916
self._update(data)
3923-
self._accessed: bool = False
39243917

39253918
def _update(self, data: GroupChannelPayload) -> None:
39263919
self.owner_id: int = int(data['owner_id'])
@@ -3940,9 +3933,6 @@ def _get_voice_state_pair(self) -> Tuple[int, int]:
39403933
return self.me.id, self.id
39413934

39423935
async def _get_channel(self) -> Self:
3943-
if not self._accessed:
3944-
await self._state.call_connect(self.id)
3945-
self._accessed = True
39463936
return self
39473937

39483938
def _initial_ring(self):

discord/client.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,38 @@ class Client:
178178
amounts of guilds. The default is ``True``.
179179
180180
.. versionadded:: 1.5
181+
guild_subscriptions: :class:`bool`
182+
Whether to subscribe to all guilds at startup.
183+
This is required to receive member events and populate the thread cache.
184+
185+
For larger servers, this is required to receive nearly all events.
186+
187+
See :doc:`guild_subscriptions` for more information.
188+
189+
.. versionadded:: 2.1
190+
191+
.. warning::
192+
193+
If this is set to ``False``, the following consequences will occur:
194+
195+
- Large guilds (over 75,000 members) will not dispatch any non-stateful events (e.g. :func:`.on_message`, :func:`.on_reaction_add`, :func:`.on_typing`, etc.)
196+
- :attr:`~Guild.threads` will only contain threads the client has joined.
197+
- Guilds will not be chunkable and member events (e.g. :func:`.on_member_update`) will not be dispatched.
198+
- Most :func:`.on_user_update` occurences will not be dispatched.
199+
- The member (:attr:`~Guild.members`) and user (:attr:`~Client.users`) cache will be largely incomplete.
200+
- Essentially, only the client user, friends/implicit relationships, voice members, and other subscribed-to users will be cached and dispatched.
201+
202+
This is useful if you want to control subscriptions manually (see :meth:`Guild.subscribe`) to save bandwidth and memory.
203+
Disabling this is not recommended for most use cases.
181204
request_guilds: :class:`bool`
182-
Whether to request guilds at startup. Defaults to True.
205+
See ``guild_subscriptions``.
183206
184207
.. versionadded:: 2.0
208+
209+
.. deprecated:: 2.1
210+
211+
This is deprecated and will be removed in a future version.
212+
Use ``guild_subscriptions`` instead.
185213
status: Optional[:class:`.Status`]
186214
A status to start your presence with upon logging on to Discord.
187215
activity: Optional[:class:`.BaseActivity`]
@@ -972,7 +1000,7 @@ def clear(self) -> None:
9721000
"""
9731001
self._closed = False
9741002
self._ready.clear()
975-
self._connection.clear()
1003+
self._connection.clear(full=True)
9761004
self.http.clear()
9771005

9781006
async def start(self, token: str, *, reconnect: bool = True) -> None:

discord/experiment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,9 @@ def __len__(self) -> int:
429429
return len(self._ids)
430430

431431
def __contains__(self, item: Union[int, Snowflake], /) -> bool:
432-
return getattr(item, 'id', item) in self._ids
432+
if isinstance(item, int):
433+
return item in self._ids
434+
return item.id in self._ids
433435

434436
def __iter__(self) -> Iterator[int]:
435437
return iter(self._ids)

discord/flags.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,20 @@ class Capabilities(BaseFlags):
301301
@classmethod
302302
def default(cls: Type[Self]) -> Self:
303303
"""Returns a :class:`Capabilities` with the current value used by the library."""
304-
return cls._from_value(8189)
304+
return cls(
305+
lazy_user_notes=True,
306+
versioned_read_states=True,
307+
versioned_user_guild_settings=True,
308+
dedupe_user_objects=True,
309+
prioritized_ready_payload=True,
310+
multiple_guild_experiment_populations=True,
311+
non_channel_read_states=True,
312+
auth_token_refresh=True,
313+
user_settings_proto=True,
314+
client_state_v2=True,
315+
passive_guild_update=True,
316+
auto_call_connect=True,
317+
)
305318

306319
@flag_value
307320
def lazy_user_notes(self):
@@ -365,10 +378,16 @@ def passive_guild_update(self):
365378
return 1 << 11
366379

367380
@flag_value
368-
def unknown_12(self):
369-
""":class:`bool`: Unknown."""
381+
def auto_call_connect(self):
382+
""":class:`bool`: Connect user to all existing calls on connect (deprecates ``CALL_CONNECT`` opcode)."""
370383
return 1 << 12
371384

385+
@flag_value
386+
def debounce_message_reactions(self):
387+
""":class:`bool`: Debounce message reactions (dispatches ``MESSAGE_REACTION_ADD_MANY`` instead of ``MESSAGE_REACTION_ADD`` when a lot of reactions are sent in quick succession)."""
388+
# Debounced reactions don't have member information, so this is kinda undesirable :(
389+
return 1 << 13
390+
372391

373392
@fill_with_flags(inverted=True)
374393
class SystemChannelFlags(BaseFlags):
@@ -1171,23 +1190,17 @@ def voice(self):
11711190
return 1
11721191

11731192
@flag_value
1174-
def other(self):
1175-
""":class:`bool`: Whether to cache members that are collected from other means.
1176-
1177-
This does not apply to members explicitly cached (e.g. :attr:`Guild.chunk`, :attr:`Guild.fetch_members`).
1193+
def joined(self):
1194+
""":class:`bool`: Whether to cache members that joined the guild
1195+
or are chunked as part of the initial log in flow.
11781196
1179-
There is an alias for this called :attr:`joined`.
1197+
Members that leave the guild are no longer cached.
11801198
"""
11811199
return 2
11821200

11831201
@alias_flag_value
1184-
def joined(self):
1185-
""":class:`bool`: Whether to cache members that are collected from other means.
1186-
1187-
This does not apply to members explicitly cached (e.g. :attr:`Guild.chunk`, :attr:`Guild.fetch_members`).
1188-
1189-
This is an alias for :attr:`other`.
1190-
"""
1202+
def other(self):
1203+
""":class:`bool`: Alias for :attr:`joined`."""
11911204
return 2
11921205

11931206
@property

discord/gateway.py

Lines changed: 18 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from .enums import Status
6262
from .state import ConnectionState
6363
from .types.snowflake import Snowflake
64+
from .types.gateway import BulkGuildSubscribePayload
6465
from .voice_client import VoiceClient
6566

6667

@@ -238,45 +239,10 @@ class DiscordWebSocket:
238239
239240
Attributes
240241
-----------
241-
DISPATCH
242-
Receive only. Denotes an event to be sent to Discord, such as READY.
243-
HEARTBEAT
244-
When received tells Discord to keep the connection alive.
245-
When sent asks if your connection is currently alive.
246-
IDENTIFY
247-
Send only. Starts a new session.
248-
PRESENCE
249-
Send only. Updates your presence.
250-
VOICE_STATE
251-
Send only. Starts a new connection to a voice guild.
252-
VOICE_PING
253-
Send only. Checks ping time to a voice guild, do not use.
254-
RESUME
255-
Send only. Resumes an existing connection.
256-
RECONNECT
257-
Receive only. Tells the client to reconnect to a new gateway.
258-
REQUEST_MEMBERS
259-
Send only. Asks for the guild members.
260-
INVALIDATE_SESSION
261-
Receive only. Tells the client to optionally invalidate the session
262-
and IDENTIFY again.
263-
HELLO
264-
Receive only. Tells the client the heartbeat interval.
265-
HEARTBEAT_ACK
266-
Receive only. Confirms receiving of a heartbeat. Not having it implies
267-
a connection issue.
268-
GUILD_SYNC
269-
Send only. Requests a guild sync. This is unfortunately no longer functional.
270-
CALL_CONNECT
271-
Send only. Maybe used for calling? Probably just tracking.
272-
GUILD_SUBSCRIBE
273-
Send only. Subscribes you to guilds/guild members. Might respond with GUILD_MEMBER_LIST_UPDATE.
274-
REQUEST_COMMANDS
275-
Send only. Requests application commands from a guild. Responds with GUILD_APPLICATION_COMMANDS_UPDATE.
276242
gateway
277243
The gateway we are currently connected to.
278244
token
279-
The authentication token for discord.
245+
The authentication token for Discord.
280246
"""
281247

282248
if TYPE_CHECKING:
@@ -307,11 +273,12 @@ class DiscordWebSocket:
307273
INVALIDATE_SESSION = 9
308274
HELLO = 10
309275
HEARTBEAT_ACK = 11
310-
GUILD_SYNC = 12 # :(
276+
# GUILD_SYNC = 12
311277
CALL_CONNECT = 13
312-
GUILD_SUBSCRIBE = 14
313-
REQUEST_COMMANDS = 24
278+
GUILD_SUBSCRIBE = 14 # Deprecated
279+
# REQUEST_COMMANDS = 24
314280
SEARCH_RECENT_MEMBERS = 35
281+
BULK_GUILD_SUBSCRIBE = 37
315282
# fmt: on
316283

317284
def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None:
@@ -727,7 +694,7 @@ async def change_presence(
727694
self.afk = afk
728695
self.idle_since = since
729696

730-
async def request_lazy_guild(
697+
async def guild_subscribe(
731698
self,
732699
guild_id: Snowflake,
733700
*,
@@ -762,6 +729,17 @@ async def request_lazy_guild(
762729
_log.debug('Subscribing to guild %s with payload %s', guild_id, payload['d'])
763730
await self.send_as_json(payload)
764731

732+
async def bulk_guild_subscribe(self, subscriptions: BulkGuildSubscribePayload) -> None:
733+
payload = {
734+
'op': self.BULK_GUILD_SUBSCRIBE,
735+
'd': {
736+
'subscriptions': subscriptions,
737+
},
738+
}
739+
740+
_log.debug('Subscribing to guilds with payload %s', payload['d'])
741+
await self.send_as_json(payload)
742+
765743
async def request_chunks(
766744
self,
767745
guild_ids: List[Snowflake],
@@ -821,44 +799,6 @@ async def call_connect(self, channel_id: Snowflake):
821799
_log.debug('Requesting call connect for channel %s.', channel_id)
822800
await self.send_as_json(payload)
823801

824-
async def request_commands(
825-
self,
826-
guild_id: Snowflake,
827-
type: int,
828-
*,
829-
nonce: Optional[str] = None,
830-
limit: Optional[int] = None,
831-
applications: Optional[bool] = None,
832-
offset: int = 0,
833-
query: Optional[str] = None,
834-
command_ids: Optional[List[Snowflake]] = None,
835-
application_id: Optional[Snowflake] = None,
836-
) -> None:
837-
payload = {
838-
'op': self.REQUEST_COMMANDS,
839-
'd': {
840-
'guild_id': str(guild_id),
841-
'type': type,
842-
},
843-
}
844-
845-
if nonce is not None:
846-
payload['d']['nonce'] = nonce
847-
if applications is not None:
848-
payload['d']['applications'] = applications
849-
if limit is not None and limit != 25:
850-
payload['d']['limit'] = limit
851-
if offset:
852-
payload['d']['offset'] = offset
853-
if query is not None:
854-
payload['d']['query'] = query
855-
if command_ids is not None:
856-
payload['d']['command_ids'] = command_ids
857-
if application_id is not None:
858-
payload['d']['application_id'] = str(application_id)
859-
860-
await self.send_as_json(payload)
861-
862802
async def search_recent_members(
863803
self, guild_id: Snowflake, query: str = '', *, after: Optional[Snowflake] = None, nonce: Optional[str] = None
864804
) -> None:

0 commit comments

Comments
 (0)