From 2da78bf2a611a7e335216ab941c2b0daa77070a4 Mon Sep 17 00:00:00 2001 From: dolfies Date: Wed, 29 Mar 2023 20:41:32 -0400 Subject: [PATCH] Implement new capabilities, PASSIVE_UPDATE_V1, and add last_pin_timestamp to channels --- discord/channel.py | 30 +++++++++++++++++++++++++++++- discord/flags.py | 7 ++++++- discord/gateway.py | 6 +++++- discord/guild.py | 15 ++++++--------- discord/state.py | 36 +++++++++++++++++++++++++++++++++++- discord/threads.py | 4 ++++ discord/types/gateway.py | 13 +++++++++++++ discord/types/voice.py | 2 +- 8 files changed, 99 insertions(+), 14 deletions(-) diff --git a/discord/channel.py b/discord/channel.py index 172137371..e0444bda3 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -153,6 +153,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): last_message_id: Optional[:class:`int`] The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. + + .. versionadded:: 2.0 slowmode_delay: :class:`int` The number of seconds a member must wait between sending messages in this channel. A value of ``0`` denotes that it is disabled. @@ -183,6 +187,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): '_overwrites', '_type', 'last_message_id', + 'last_pin_timestamp', 'default_auto_archive_duration', 'default_thread_slowmode_delay', ) @@ -218,6 +223,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): self.default_thread_slowmode_delay: int = data.get('default_thread_rate_limit_per_user', 0) self._type: Literal[0, 5] = data.get('type', self._type) self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('last_pin_timestamp')) self._fill_overwrites(data) async def _get_channel(self) -> Self: @@ -883,6 +889,7 @@ class VocalGuildChannel(discord.abc.Messageable, discord.abc.Connectable, discor 'rtc_region', 'video_quality_mode', 'last_message_id', + 'last_pin_timestamp', ) def __init__(self, *, state: ConnectionState, guild: Guild, data: Union[VoiceChannelPayload, StageChannelPayload]): @@ -907,6 +914,7 @@ class VocalGuildChannel(discord.abc.Messageable, discord.abc.Connectable, discor self.video_quality_mode: VideoQualityMode = try_enum(VideoQualityMode, data.get('video_quality_mode', 1)) self.category_id: Optional[int] = utils._get_as_snowflake(data, 'parent_id') self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('last_pin_timestamp')) self.position: int = data['position'] self.slowmode_delay = data.get('rate_limit_per_user', 0) self.bitrate: int = data['bitrate'] @@ -1262,6 +1270,10 @@ class VoiceChannel(VocalGuildChannel): The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. + .. versionadded:: 2.0 + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. + .. versionadded:: 2.0 slowmode_delay: :class:`int` The number of seconds a member must wait between sending messages @@ -1462,6 +1474,10 @@ class StageChannel(VocalGuildChannel): The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. + .. versionadded:: 2.0 + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. + .. versionadded:: 2.0 slowmode_delay: :class:`int` The number of seconds a member must wait between sending messages @@ -2794,6 +2810,10 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. + .. versionadded:: 2.0 + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. + .. versionadded:: 2.0 """ @@ -2802,6 +2822,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr 'recipient', 'me', 'last_message_id', + 'last_pin_timestamp', '_message_request', '_requested_at', '_spam', @@ -2819,6 +2840,7 @@ class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.Pr def _update(self, data: DMChannelPayload) -> None: self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('last_pin_timestamp')) self._message_request: Optional[bool] = data.get('is_message_request') self._requested_at: Optional[datetime.datetime] = utils.parse_time(data.get('is_message_request_timestamp')) self._spam: bool = data.get('is_spam', False) @@ -3130,6 +3152,10 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc The last message ID of the message sent to this channel. It may *not* point to an existing or valid message. + .. versionadded:: 2.0 + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. + .. versionadded:: 2.0 recipients: List[:class:`User`] The users you are participating with in the group channel. @@ -3162,6 +3188,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc __slots__ = ( 'last_message_id', + 'last_pin_timestamp', 'id', 'recipients', 'owner_id', @@ -3188,6 +3215,7 @@ class GroupChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc self.name: Optional[str] = data.get('name') self.recipients: List[User] = [self._state.store_user(u) for u in data.get('recipients', [])] self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id') + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time(data.get('last_pin_timestamp')) self.managed: bool = data.get('managed', False) self.application_id: Optional[int] = utils._get_as_snowflake(data, 'application_id') self.nicks: Dict[User, str] = {utils.get(self.recipients, id=int(k)): v for k, v in data.get('nicks', {}).items()} # type: ignore @@ -3606,6 +3634,7 @@ class PartialMessageable(discord.abc.Messageable, Hashable): self.guild_id: Optional[int] = guild_id self.type: Optional[ChannelType] = type self.last_message_id: Optional[int] = None + self.last_pin_timestamp: Optional[datetime.datetime] = None def __repr__(self) -> str: return f'<{self.__class__.__name__} id={self.id} type={self.type!r}>' @@ -3649,7 +3678,6 @@ class PartialMessageable(discord.abc.Messageable, Hashable): :class:`Permissions` The resolved permissions. """ - return Permissions.none() def get_partial_message(self, message_id: int, /) -> PartialMessage: diff --git a/discord/flags.py b/discord/flags.py index ac32c3e1f..84cb3337d 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -272,7 +272,7 @@ class Capabilities(BaseFlags): @classmethod def default(cls: Type[Self]) -> Self: """Returns a :class:`Capabilities` with the current value used by the library.""" - return cls._from_value(2045) + return cls._from_value(8189) @flag_value def lazy_user_notes(self): @@ -335,6 +335,11 @@ class Capabilities(BaseFlags): """:class:`bool`: Enable passive guild update (replace ``CHANNEL_UNREADS_UPDATE`` with ``PASSIVE_UPDATE_V1``, a similar event that includes a ``voice_states`` array and a ``members`` array that includes the members of aforementioned voice states).""" return 1 << 11 + @flag_value + def unknown_12(self): + """:class:`bool`: Unknown.""" + return 1 << 12 + @fill_with_flags(inverted=True) class SystemChannelFlags(BaseFlags): diff --git a/discord/gateway.py b/discord/gateway.py index af22d0a16..92158f0a4 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -469,6 +469,7 @@ class DiscordWebSocket: # presence['status'] = self._connection._status or 'unknown' # presence['activities'] = self._connection._activities + # TODO: Implement client state payload = { 'op': self.IDENTIFY, 'd': { @@ -478,10 +479,13 @@ class DiscordWebSocket: 'presence': presence, 'compress': not self._zlib_enabled, # We require at least one form of compression 'client_state': { - 'guild_hashes': {}, + 'api_code_version': 0, + 'guild_versions': {}, 'highest_last_message_id': '0', + 'private_channels_version': '0', 'read_state_version': 0, 'user_guild_settings_version': -1, + 'user_settings_version': -1, }, }, } diff --git a/discord/guild.py b/discord/guild.py index b4dca52fe..dbb55e797 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -500,9 +500,6 @@ class Guild(Hashable): stage_instance = StageInstance(guild=self, data=s, state=state) self._stage_instances[stage_instance.id] = stage_instance - for vs in guild.get('voice_states', []): - self._update_voice_state(vs, int(vs['channel_id'])) - for s in guild.get('guild_scheduled_events', []): scheduled_event = ScheduledEvent(data=s, state=state) self._scheduled_events[scheduled_event.id] = scheduled_event @@ -549,13 +546,13 @@ class Guild(Hashable): if (counts := guild.get('application_command_counts')) is not None: self.command_counts = CommandCounts(counts.get(0, 0), counts.get(1, 0), counts.get(2, 0)) + for vs in guild.get('voice_states', []): + self._update_voice_state(vs, int(vs['channel_id'])) + cache_flags = state.member_cache_flags - if cache_flags.other: - for mdata in guild.get('members', []): - try: - member = Member(data=mdata, guild=self, state=state) - except KeyError: - continue + for mdata in guild.get('members', []): + member = Member(data=mdata, guild=self, state=state) + if cache_flags.joined or member.id == state.self_id or (cache_flags.voice and member.id in self._voice_states): self._add_member(member) for presence in guild.get('presences', []): diff --git a/discord/state.py b/discord/state.py index 05a35d9a6..61e9f6382 100644 --- a/discord/state.py +++ b/discord/state.py @@ -600,6 +600,7 @@ class ConnectionState: self.analytics_token: Optional[str] = None self.preferred_regions: List[str] = [] self.country_code: Optional[str] = None + self.api_code_version: int = 0 self.session_type: Optional[str] = None self.auth_session_id: Optional[str] = None self._emojis: Dict[int, Emoji] = {} @@ -977,6 +978,9 @@ class ConnectionState: for presence in merged_presences: presence['user'] = {'id': presence['user_id']} # type: ignore # :( + if 'properties' in guild_data: + guild_data.update(guild_data.pop('properties')) # type: ignore # :( + voice_states = guild_data.setdefault('voice_states', []) voice_states.extend(guild_extra.get('voice_states', [])) members = guild_data.setdefault('members', []) @@ -1036,6 +1040,7 @@ class ConnectionState: } self.consents = TrackingSettings(data=data.get('consents', {}), state=self) self.country_code = data.get('country_code', 'US') + self.api_code_version = data.get('api_code_version', 1) self.session_type = data.get('session_type', 'normal') self.auth_session_id = data.get('auth_session_id_hash') self.connections = {c['id']: Connection(state=self, data=c) for c in data.get('connected_accounts', [])} @@ -1059,6 +1064,31 @@ class ConnectionState: def parse_resumed(self, data: gw.ResumedEvent) -> None: self.dispatch('resumed') + def parse_passive_update_v1(self, data: gw.PassiveUpdateEvent) -> None: + # PASSIVE_UPDATE_V1 is sent for large guilds you are not subscribed to + # in order to keep their read and voice states up-to-date; it replaces CHANNEL_UNREADS_UPDATE + guild = self._get_guild(int(data['guild_id'])) + if not guild: + _log.debug('PASSIVE_UPDATE_V1 referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + return + + for channel_data in data.get('channels', []): + channel = guild.get_channel(int(channel_data['id'])) + if not channel: + continue + channel.last_message_id = utils._get_as_snowflake(channel_data, 'last_message_id') # type: ignore + if 'last_pin_timestamp' in channel_data and hasattr(channel, 'last_pin_timestamp'): + channel.last_pin_timestamp = utils.parse_time(channel_data['last_pin_timestamp']) # type: ignore + + guild._voice_states = {} + members = {int(m['user']['id']): m for m in data.get('members', [])} + for voice_state in data.get('voice_states', []): + user_id = int(voice_state['user_id']) + member = members.get(user_id) + if member: + voice_state['member'] = member + guild._update_voice_state(voice_state, utils._get_as_snowflake(voice_state, 'channel_id')) + def parse_message_create(self, data: gw.MessageCreateEvent) -> None: guild_id = utils._get_as_snowflake(data, 'guild_id') channel, _ = self._get_guild_channel(data) @@ -1529,6 +1559,8 @@ class ConnectionState: return last_pin = utils.parse_time(data.get('last_pin_timestamp')) + if hasattr(channel, 'last_pin_timestamp'): + channel.last_pin_timestamp = last_pin # type: ignore # Handled above if guild is None: self.dispatch('private_channel_pins_update', channel, last_pin) @@ -2193,8 +2225,10 @@ class ConnectionState: self.dispatch('guild_join', guild) def parse_guild_create(self, data: gw.GuildCreateEvent): - guild = self._get_create_guild(data) + if 'properties' in data: + data.update(data.pop('properties')) # type: ignore + guild = self._get_create_guild(data) if guild is None: return diff --git a/discord/threads.py b/discord/threads.py index 77907b943..054552be6 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -100,6 +100,8 @@ class Thread(Messageable, Hashable): last_message_id: Optional[:class:`int`] The last message ID of the message sent to this thread. It may *not* point to an existing or valid message. + last_pin_timestamp: Optional[:class:`datetime.datetime`] + When the last pinned message was pinned. ``None`` if there are no pinned messages. slowmode_delay: :class:`int` The number of seconds a member must wait between sending messages in this thread. A value of ``0`` denotes that it is disabled. @@ -133,6 +135,7 @@ class Thread(Messageable, Hashable): 'owner_id', 'parent_id', 'last_message_id', + 'last_pin_timestamp', 'message_count', 'member_count', 'slowmode_delay', @@ -172,6 +175,7 @@ class Thread(Messageable, Hashable): self.name: str = data['name'] self._type: ChannelType = try_enum(ChannelType, data['type']) self.last_message_id: Optional[int] = _get_as_snowflake(data, 'last_message_id') + self.last_pin_timestamp: Optional[datetime] = parse_time(data.get('last_pin_timestamp')) self.slowmode_delay: int = data.get('rate_limit_per_user', 0) self.message_count: int = data['message_count'] self.member_count: int = data['member_count'] diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 74c2a8b3f..45aa83be2 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -485,3 +485,16 @@ class AutoModerationActionExecution(TypedDict): class GuildAuditLogEntryCreate(AuditLogEntry): guild_id: Snowflake + + +class PartialUpdateChannel(TypedDict): + id: Snowflake + last_message_id: Optional[Snowflake] + last_pin_timestamp: NotRequired[Optional[str]] + + +class PassiveUpdateEvent(TypedDict): + guild_id: Snowflake + channels: List[PartialUpdateChannel] + voice_states: NotRequired[List[GuildVoiceState]] + members: NotRequired[List[MemberWithUser]] diff --git a/discord/types/voice.py b/discord/types/voice.py index d68f237d8..cb9e412cc 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -50,7 +50,7 @@ class GuildVoiceState(_VoiceState): class VoiceState(_VoiceState, total=False): - channel_id: Optional[Snowflake] + channel_id: NotRequired[Optional[Snowflake]] guild_id: NotRequired[Optional[Snowflake]]