From 467bca421005d48281edde8fede1d12df717904c Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:04:19 +1000 Subject: [PATCH 01/37] Add client docs. --- discord/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/discord/client.py b/discord/client.py index ff02bf7b6f00..139b5ef94bb0 100644 --- a/discord/client.py +++ b/discord/client.py @@ -237,6 +237,15 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 + enable_raw_presence_event: :class:`bool` + Whether to manually enable or disable the :func:`on_raw_presence_update` event. + + Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. + + By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` + is disabled, otherwise set to ``False``. + + .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``. This allows you to check requests the library is using. For more information, check the From 21c0bd0118f4e8343f2b85888b97a8f1bd60b916 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:04:48 +1000 Subject: [PATCH 02/37] Fix is_on_mobile docs --- discord/member.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/discord/member.py b/discord/member.py index 388e854835e5..482bbf12c814 100644 --- a/discord/member.py +++ b/discord/member.py @@ -549,7 +549,12 @@ def web_status(self) -> Status: return try_enum(Status, self._client_status.web or 'offline') def is_on_mobile(self) -> bool: - """:class:`bool`: A helper function that determines if a member is active on a mobile device.""" + """A helper function that determines if a member is active on a mobile device. + + Returns + ------- + :class:`bool` + """ return self._client_status.mobile is not None @property From 9697fcafdb556a73ccf5799c761db2de49a1c4de Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:05:33 +1000 Subject: [PATCH 03/37] Add RawPresenceUpdateEvent model --- discord/raw_models.py | 88 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 8d3ad328fb4c..648106f0f66c 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -27,10 +27,12 @@ import datetime from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union -from .enums import ChannelType, try_enum, ReactionType +from .enums import ChannelType, try_enum, ReactionType, Status +from .activity import create_activity from .utils import _get_as_snowflake from .app_commands import AppCommandPermissions from .colour import Colour +from .member import _ClientStatus if TYPE_CHECKING: from typing_extensions import Self @@ -51,6 +53,8 @@ GuildMemberRemoveEvent, PollVoteActionEvent, ) + from .types.activity import PartialPresenceUpdate + from .activity import ActivityTypes from .types.command import GuildApplicationCommandPermissions from .message import Message from .partial_emoji import PartialEmoji @@ -79,6 +83,7 @@ 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', 'RawPollVoteActionEvent', + 'RawPresenceUpdateEvent', ) @@ -557,3 +562,84 @@ def __init__(self, data: PollVoteActionEvent) -> None: self.message_id: int = int(data['message_id']) self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') self.answer_id: int = int(data['answer_id']) + + +class RawPresenceUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_presence_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that triggered the presence update. + guild_id: Optional[:class:`int`] + The guild ID for the users presence update. Could be ``None``. + """ + + __slots__ = ('user_id', 'guild_id', '_client_status', '_raw_activities', '_state', '_activities') + + def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self.user_id: int = int(data["user"]["id"]) + + self._client_status: _ClientStatus = _ClientStatus() + self._client_status._update(data["status"], data["client_status"]) + + self._raw_activities = data['activities'] + self._state = state + + try: + self.guild_id: Optional[int] = int(data['guild_id']) + except KeyError: + self.guild_id = None + + self._activities = None + + @property + def activities(self) -> Tuple[ActivityTypes, ...]: + """Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]: The activities the user is currently doing. + + .. note:: + + Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer + than ``128`` characters. See :issue:`1738` for more information. + """ + if self._activities is None: + self._activities = tuple(create_activity(d, self._state) for d in self._raw_activities) + + return self._activities + + @property + def status(self) -> Status: + """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" + return try_enum(Status, self._client_status._status) + + @property + def raw_status(self) -> str: + """:class:`str`: The user's overall status as a string value.""" + return self._client_status._status + + @property + def mobile_status(self) -> Status: + """:class:`Status`: The user's status on a mobile device, if applicable.""" + return try_enum(Status, self._client_status.mobile or 'offline') + + @property + def desktop_status(self) -> Status: + """:class:`Status`: The user's status on the desktop client, if applicable.""" + return try_enum(Status, self._client_status.desktop or 'offline') + + @property + def web_status(self) -> Status: + """:class:`Status`: The user's status on the web client, if applicable.""" + return try_enum(Status, self._client_status.web or 'offline') + + def is_on_mobile(self) -> bool: + """A helper function that determines if a user is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self._client_status.mobile is not None From c058ff6a320a01b1021d42aaa5914231fe5dd67d Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:06:12 +1000 Subject: [PATCH 04/37] Add raw_presence flags and event dispatch to state --- discord/state.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/discord/state.py b/discord/state.py index df6073985d7c..a664da264f77 100644 --- a/discord/state.py +++ b/discord/state.py @@ -261,6 +261,10 @@ def __init__( if not intents.members or cache_flags._empty: self.store_user = self.store_user_no_intents + self.raw_presence_flag: bool = options.get('enable_raw_presence_event', utils.MISSING) + if self.raw_presence_flag is utils.MISSING: + self.raw_presence_flag = not intents.members and intents.presences + self.parsers: Dict[str, Callable[[Any], None]] self.parsers = parsers = {} for attr, func in inspect.getmembers(self): @@ -802,6 +806,9 @@ def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None: self.dispatch('interaction', interaction) def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: + if self.raw_presence_flag: + self.dispatch('raw_presence_update', RawPresenceUpdateEvent(data=data, state=self)) + guild_id = utils._get_as_snowflake(data, 'guild_id') # guild_id won't be None here guild = self._get_guild(guild_id) From 646e800c1434bd676c2cbbb41961503eac80c163 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:06:48 +1000 Subject: [PATCH 05/37] Add docs. --- docs/api.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index ca0fb4ef4645..e14e57593ee9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -916,6 +916,29 @@ Members :param after: The updated member's updated info. :type after: :class:`Member` +.. function:: on_raw_presence_update(payload) + + Called when a :class:`Member` updates their presence. + + This requires :attr:`Intents.presences` to be enabled. + + Unlike :func:`on_presence_update`, when enabled, this is called regardless of the state of internal guild + and member caches, and **does not** provide a comparison between the previous and updated states of the :class:`Member`. + + .. important:: + + By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` + is disabled. + + You can maually override this behaviour by setting the **enable_raw_presence_event** flag in the :class:`Client`, + however :attr:`Intents.presences` is always required for this event to work. + + .. versionadded:: 2.5 + + :param payload: The raw presence update event payload class. + :type payload: :class:`RawPresenceUpdateEvent` + + Messages ~~~~~~~~~ @@ -5353,6 +5376,14 @@ RawPollVoteActionEvent .. autoclass:: RawPollVoteActionEvent() :members: +RawPresenceUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPresenceUpdateEvent + +.. autoclass:: RawPresenceUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ From c3639a778e1fb9d81f237610becbebf2956c3c63 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:07:54 +1000 Subject: [PATCH 06/37] Run black --- discord/member.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/member.py b/discord/member.py index 482bbf12c814..673554be0614 100644 --- a/discord/member.py +++ b/discord/member.py @@ -550,7 +550,7 @@ def web_status(self) -> Status: def is_on_mobile(self) -> bool: """A helper function that determines if a member is active on a mobile device. - + Returns ------- :class:`bool` diff --git a/setup.py b/setup.py index 2481afeb428c..e3d6d59f4fff 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup import re + def derive_version() -> str: version = '' with open('discord/__init__.py') as f: From dd22731062f27bbd8a8935eaccb8f62ae21793b0 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:13:05 +1000 Subject: [PATCH 07/37] Simplify presence model --- discord/raw_models.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 648106f0f66c..a4de00f24e3f 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -577,24 +577,20 @@ class RawPresenceUpdateEvent(_RawReprMixin): The guild ID for the users presence update. Could be ``None``. """ - __slots__ = ('user_id', 'guild_id', '_client_status', '_raw_activities', '_state', '_activities') + __slots__ = ('user_id', 'guild_id', '_client_status', '_activities') def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: self.user_id: int = int(data["user"]["id"]) self._client_status: _ClientStatus = _ClientStatus() self._client_status._update(data["status"], data["client_status"]) - - self._raw_activities = data['activities'] - self._state = state + self._activities = tuple(create_activity(d, state) for d in data['activities']) try: self.guild_id: Optional[int] = int(data['guild_id']) except KeyError: self.guild_id = None - self._activities = None - @property def activities(self) -> Tuple[ActivityTypes, ...]: """Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]: The activities the user is currently doing. @@ -605,9 +601,6 @@ def activities(self) -> Tuple[ActivityTypes, ...]: if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information. """ - if self._activities is None: - self._activities = tuple(create_activity(d, self._state) for d in self._raw_activities) - return self._activities @property From e1bd692a59a18fb7e39f867d1bcfc9608ea62fe9 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:40:20 +1000 Subject: [PATCH 08/37] Move presences models around --- discord/__init__.py | 1 + discord/guild.py | 8 +- discord/member.py | 72 +++++------------ discord/presences.py | 179 ++++++++++++++++++++++++++++++++++++++++++ discord/raw_models.py | 93 +--------------------- discord/state.py | 22 +++--- discord/utils.py | 8 ++ 7 files changed, 226 insertions(+), 157 deletions(-) create mode 100644 discord/presences.py diff --git a/discord/__init__.py b/discord/__init__.py index c206f650f66f..f850ee4acbea 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -72,6 +72,7 @@ from .poll import * from .soundboard import * from .subscription import * +from .presences import * class VersionInfo(NamedTuple): diff --git a/discord/guild.py b/discord/guild.py index fc39179abeb2..d8291405b86c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,6 +95,7 @@ from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound +from .presences import RawPresenceUpdateEvent __all__ = ( @@ -536,10 +537,11 @@ def _from_data(self, guild: GuildPayload) -> None: empty_tuple = () for presence in guild.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) + raw = RawPresenceUpdateEvent(data=presence, state=self._state) + member = self.get_member(raw.user_id) + if member is not None: - member._presence_update(presence, empty_tuple) # type: ignore + member._presence_update(presence, raw, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] diff --git a/discord/member.py b/discord/member.py index 673554be0614..a1899b0a5468 100644 --- a/discord/member.py +++ b/discord/member.py @@ -36,13 +36,13 @@ from .asset import Asset from .utils import MISSING from .user import BaseUser, ClientUser, User, _UserTag -from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import Status, try_enum +from .enums import Status from .errors import ClientException from .colour import Colour from .object import Object from .flags import MemberFlags +from .presences import _ClientStatus __all__ = ( 'VoiceState', @@ -57,16 +57,14 @@ from .channel import DMChannel, VoiceChannel, StageChannel from .flags import PublicUserFlags from .guild import Guild - from .types.activity import ( - ClientStatus as ClientStatusPayload, - PartialPresenceUpdate, - ) + from .activity import ActivityTypes + from .presences import RawPresenceUpdateEvent from .types.member import ( MemberWithUser as MemberWithUserPayload, Member as MemberPayload, UserWithMember as UserWithMemberPayload, ) - from .types.gateway import GuildMemberUpdateEvent + from .types.gateway import GuildMemberUpdateEvent, PartialPresenceUpdate from .types.user import User as UserPayload, AvatarDecorationData from .abc import Snowflake from .state import ConnectionState @@ -168,46 +166,6 @@ def __repr__(self) -> str: return f'<{self.__class__.__name__} {inner}>' -class _ClientStatus: - __slots__ = ('_status', 'desktop', 'mobile', 'web') - - def __init__(self): - self._status: str = 'offline' - - self.desktop: Optional[str] = None - self.mobile: Optional[str] = None - self.web: Optional[str] = None - - def __repr__(self) -> str: - attrs = [ - ('_status', self._status), - ('desktop', self.desktop), - ('mobile', self.mobile), - ('web', self.web), - ] - inner = ' '.join('%s=%r' % t for t in attrs) - return f'<{self.__class__.__name__} {inner}>' - - def _update(self, status: str, data: ClientStatusPayload, /) -> None: - self._status = status - - self.desktop = data.get('desktop') - self.mobile = data.get('mobile') - self.web = data.get('web') - - @classmethod - def _copy(cls, client_status: Self, /) -> Self: - self = cls.__new__(cls) # bypass __init__ - - self._status = client_status._status - - self.desktop = client_status.desktop - self.mobile = client_status.mobile - self.web = client_status.web - - return self - - def flatten_user(cls: T) -> T: for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): # ignore private/special methods @@ -473,12 +431,18 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') - def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: - self.activities = tuple(create_activity(d, self._state) for d in data['activities']) - self._client_status._update(data['status'], data['client_status']) + def _presence_update( + self, data: PartialPresenceUpdate, raw: RawPresenceUpdateEvent, user: UserPayload + ) -> Optional[Tuple[User, User]]: + if raw._activities is None: + raw._create_activities(data, self._state) + + self.activities = raw.activities + self._client_status = _ClientStatus._copy(raw.client_status) if len(user) > 1: return self._update_inner_user(user) + return None def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: @@ -518,7 +482,7 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: @property def status(self) -> Status: """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return try_enum(Status, self._client_status._status) + return self._client_status.status @property def raw_status(self) -> str: @@ -536,17 +500,17 @@ def status(self, value: Status) -> None: @property def mobile_status(self) -> Status: """:class:`Status`: The member's status on a mobile device, if applicable.""" - return try_enum(Status, self._client_status.mobile or 'offline') + return self._client_status.mobile_status @property def desktop_status(self) -> Status: """:class:`Status`: The member's status on the desktop client, if applicable.""" - return try_enum(Status, self._client_status.desktop or 'offline') + return self._client_status.desktop_status @property def web_status(self) -> Status: """:class:`Status`: The member's status on the web client, if applicable.""" - return try_enum(Status, self._client_status.web or 'offline') + return self._client_status.web_status def is_on_mobile(self) -> bool: """A helper function that determines if a member is active on a mobile device. diff --git a/discord/presences.py b/discord/presences.py new file mode 100644 index 000000000000..bda60f7af462 --- /dev/null +++ b/discord/presences.py @@ -0,0 +1,179 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Tuple + +from .activity import create_activity +from .enums import Status, try_enum +from .utils import _get_as_snowflake, _RawReprMixin + +if TYPE_CHECKING: + from typing_extensions import Self + + from .activity import ActivityTypes + from .guild import Guild + from .state import ConnectionState + from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate + + +__all__ = ('RawPresenceUpdateEvent',) + + +class _ClientStatus: + __slots__ = ('_status', 'desktop', 'mobile', 'web') + + def __init__(self): + self._status: str = 'offline' + + self.desktop: Optional[str] = None + self.mobile: Optional[str] = None + self.web: Optional[str] = None + + def __repr__(self) -> str: + attrs = [ + ('_status', self._status), + ('desktop', self.desktop), + ('mobile', self.mobile), + ('web', self.web), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {inner}>' + + def _update(self, status: str, data: ClientStatusPayload, /) -> None: + self._status = status + + self.desktop = data.get('desktop') + self.mobile = data.get('mobile') + self.web = data.get('web') + + @classmethod + def _copy(cls, client_status: Self, /) -> Self: + self = cls.__new__(cls) # bypass __init__ + + self._status = client_status._status + + self.desktop = client_status.desktop + self.mobile = client_status.mobile + self.web = client_status.web + + return self + + @property + def status(self) -> Status: + return try_enum(Status, self._status) + + @property + def raw_status(self) -> str: + return self._status + + @property + def mobile_status(self) -> Status: + return try_enum(Status, self.mobile or 'offline') + + @property + def desktop_status(self) -> Status: + return try_enum(Status, self.desktop or 'offline') + + @property + def web_status(self) -> Status: + return try_enum(Status, self.web or 'offline') + + +class RawPresenceUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_presence_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that triggered the presence update. + guild_id: Optional[:class:`int`] + The guild ID for the users presence update. Could be ``None``. + """ + + __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', '_activities') + + def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self.user_id: int = int(data["user"]["id"]) + + self.client_status: _ClientStatus = _ClientStatus() + self.client_status._update(data["status"], data["client_status"]) + self._activities: Tuple[ActivityTypes, ...] | None = None + + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.guild: Guild | None = state._get_guild(self.guild_id) + + def _create_activities(self, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self._activities = tuple(create_activity(d, state) for d in data['activities']) + + @property + def activities(self) -> Tuple[ActivityTypes, ...]: + """Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]: The activities the user is currently doing. + + .. note:: + + Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer + than ``128`` characters. See :issue:`1738` for more information. + """ + return self._activities or () + + @property + def status(self) -> Status: + """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" + return self.client_status.status + + @property + def raw_status(self) -> str: + """:class:`str`: The member's overall status as a string value. + + .. versionadded:: 1.5 + """ + return self.client_status._status + + @property + def mobile_status(self) -> Status: + """:class:`Status`: The member's status on a mobile device, if applicable.""" + return self.client_status.mobile_status + + @property + def desktop_status(self) -> Status: + """:class:`Status`: The member's status on the desktop client, if applicable.""" + return self.client_status.desktop_status + + @property + def web_status(self) -> Status: + """:class:`Status`: The member's status on the web client, if applicable.""" + return self.client_status.web_status + + def is_on_mobile(self) -> bool: + """A helper function that determines if a member is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self.client_status.mobile is not None diff --git a/discord/raw_models.py b/discord/raw_models.py index a4de00f24e3f..6121e68ad3bc 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -25,14 +25,12 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union +from typing import TYPE_CHECKING, Literal, Optional, Set, List, Union -from .enums import ChannelType, try_enum, ReactionType, Status -from .activity import create_activity -from .utils import _get_as_snowflake +from .enums import ChannelType, try_enum, ReactionType +from .utils import _get_as_snowflake, _RawReprMixin from .app_commands import AppCommandPermissions from .colour import Colour -from .member import _ClientStatus if TYPE_CHECKING: from typing_extensions import Self @@ -53,8 +51,6 @@ GuildMemberRemoveEvent, PollVoteActionEvent, ) - from .types.activity import PartialPresenceUpdate - from .activity import ActivityTypes from .types.command import GuildApplicationCommandPermissions from .message import Message from .partial_emoji import PartialEmoji @@ -83,18 +79,9 @@ 'RawMemberRemoveEvent', 'RawAppCommandPermissionsUpdateEvent', 'RawPollVoteActionEvent', - 'RawPresenceUpdateEvent', ) -class _RawReprMixin: - __slots__: Tuple[str, ...] = () - - def __repr__(self) -> str: - value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) - return f'<{self.__class__.__name__} {value}>' - - class RawMessageDeleteEvent(_RawReprMixin): """Represents the event payload for a :func:`on_raw_message_delete` event. @@ -562,77 +549,3 @@ def __init__(self, data: PollVoteActionEvent) -> None: self.message_id: int = int(data['message_id']) self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') self.answer_id: int = int(data['answer_id']) - - -class RawPresenceUpdateEvent(_RawReprMixin): - """Represents the payload for a :func:`on_raw_presence_update` event. - - .. versionadded:: 2.5 - - Attributes - ---------- - user_id: :class:`int` - The ID of the user that triggered the presence update. - guild_id: Optional[:class:`int`] - The guild ID for the users presence update. Could be ``None``. - """ - - __slots__ = ('user_id', 'guild_id', '_client_status', '_activities') - - def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: - self.user_id: int = int(data["user"]["id"]) - - self._client_status: _ClientStatus = _ClientStatus() - self._client_status._update(data["status"], data["client_status"]) - self._activities = tuple(create_activity(d, state) for d in data['activities']) - - try: - self.guild_id: Optional[int] = int(data['guild_id']) - except KeyError: - self.guild_id = None - - @property - def activities(self) -> Tuple[ActivityTypes, ...]: - """Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]: The activities the user is currently doing. - - .. note:: - - Due to a Discord API limitation, a user's Spotify activity may not appear - if they are listening to a song with a title longer - than ``128`` characters. See :issue:`1738` for more information. - """ - return self._activities - - @property - def status(self) -> Status: - """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return try_enum(Status, self._client_status._status) - - @property - def raw_status(self) -> str: - """:class:`str`: The user's overall status as a string value.""" - return self._client_status._status - - @property - def mobile_status(self) -> Status: - """:class:`Status`: The user's status on a mobile device, if applicable.""" - return try_enum(Status, self._client_status.mobile or 'offline') - - @property - def desktop_status(self) -> Status: - """:class:`Status`: The user's status on the desktop client, if applicable.""" - return try_enum(Status, self._client_status.desktop or 'offline') - - @property - def web_status(self) -> Status: - """:class:`Status`: The user's status on the web client, if applicable.""" - return try_enum(Status, self._client_status.web or 'offline') - - def is_on_mobile(self) -> bool: - """A helper function that determines if a user is active on a mobile device. - - Returns - ------- - :class:`bool` - """ - return self._client_status.mobile is not None diff --git a/discord/state.py b/discord/state.py index a664da264f77..e1764c5eb4ed 100644 --- a/discord/state.py +++ b/discord/state.py @@ -62,6 +62,7 @@ from .channel import * from .channel import _channel_factory from .raw_models import * +from .presences import RawPresenceUpdateEvent from .member import Member from .role import Role from .enums import ChannelType, try_enum, Status @@ -806,25 +807,26 @@ def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None: self.dispatch('interaction', interaction) def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: + raw = RawPresenceUpdateEvent(data=data, state=self) + if self.raw_presence_flag: - self.dispatch('raw_presence_update', RawPresenceUpdateEvent(data=data, state=self)) + raw._create_activities(data, self) + self.dispatch('raw_presence_update', raw) - guild_id = utils._get_as_snowflake(data, 'guild_id') - # guild_id won't be None here - guild = self._get_guild(guild_id) - if guild is None: - _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) + if raw.guild is None: + _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) return user = data['user'] - member_id = int(user['id']) - member = guild.get_member(member_id) + member = raw.guild.get_member(raw.user_id) + if member is None: - _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id) + _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', raw.user_id) return old_member = Member._copy(member) - user_update = member._presence_update(data=data, user=user) + user_update = member._presence_update(data, raw=raw, user=user) + if user_update: self.dispatch('user_update', user_update[0], user_update[1]) diff --git a/discord/utils.py b/discord/utils.py index 905735cfb406..af236a91759d 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1531,3 +1531,11 @@ def _format_call_duration(duration: datetime.timedelta) -> str: formatted = f"{years} years" return formatted + + +class _RawReprMixin: + __slots__: Tuple[str, ...] = () + + def __repr__(self) -> str: + value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) + return f'<{self.__class__.__name__} {value}>' From a1ef8f42c504a8ca85040081992f6017b0e02cd8 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:41:57 +1000 Subject: [PATCH 09/37] Dispatch copy to raw event --- discord/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/state.py b/discord/state.py index e1764c5eb4ed..ea1758b0fc83 100644 --- a/discord/state.py +++ b/discord/state.py @@ -811,7 +811,7 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: if self.raw_presence_flag: raw._create_activities(data, self) - self.dispatch('raw_presence_update', raw) + self.dispatch('raw_presence_update', RawPresenceUpdateEvent._copy(raw)) if raw.guild is None: _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) From c7e34e62da345c0f36c25230941983c26cad68bf Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:43:32 +1000 Subject: [PATCH 10/37] Expose ClientStatus and make it available on Member --- discord/member.py | 17 ++++++-- discord/presences.py | 93 +++++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/discord/member.py b/discord/member.py index a1899b0a5468..e5178f77698f 100644 --- a/discord/member.py +++ b/discord/member.py @@ -42,7 +42,7 @@ from .colour import Colour from .object import Object from .flags import MemberFlags -from .presences import _ClientStatus +from .presences import ClientStatus __all__ = ( 'VoiceState', @@ -312,7 +312,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at')) self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since')) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles'])) - self._client_status: _ClientStatus = _ClientStatus() + self._client_status: ClientStatus = ClientStatus() self.activities: Tuple[ActivityTypes, ...] = () self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) @@ -388,7 +388,7 @@ def _copy(cls, member: Self) -> Self: self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self._client_status = _ClientStatus._copy(member._client_status) + self._client_status = ClientStatus._copy(member._client_status) self.guild = member.guild self.nick = member.nick self.pending = member.pending @@ -438,7 +438,7 @@ def _presence_update( raw._create_activities(data, self._state) self.activities = raw.activities - self._client_status = _ClientStatus._copy(raw.client_status) + self._client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) @@ -478,6 +478,15 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: ) # Signal to dispatch on_user_update return to_return, u + + @property + def client_status(self) -> ClientStatus: + """:class:`ClientStatus`: Model which holds information about the status of the + member on various clients. + + .. versionadded:: 2.5 + """ + return self._client_status @property def status(self) -> Status: diff --git a/discord/presences.py b/discord/presences.py index bda60f7af462..37e39617299b 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -27,7 +27,7 @@ from .activity import create_activity from .enums import Status, try_enum -from .utils import _get_as_snowflake, _RawReprMixin +from .utils import MISSING, _get_as_snowflake, _RawReprMixin if TYPE_CHECKING: from typing_extensions import Self @@ -41,15 +41,25 @@ __all__ = ('RawPresenceUpdateEvent',) -class _ClientStatus: +class ClientStatus: + """The :class:`ClientStatus` model which holds information about the status of the user on various clients. + + .. note:: + + You usually wouldn't instantiate this class manually, instead it should be available on members with + :attr:`Member.client_status` and on the :class:`RawPresenceUpdateEvent` model. + + .. versionadded:: 2.5 + """ __slots__ = ('_status', 'desktop', 'mobile', 'web') - def __init__(self): - self._status: str = 'offline' + def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None: + self._status: str = status or 'offline' - self.desktop: Optional[str] = None - self.mobile: Optional[str] = None - self.web: Optional[str] = None + data = data or {} + self.desktop: Optional[str] = data.get('desktop') + self.mobile: Optional[str] = data.get('mobile') + self.web: Optional[str] = data.get('web') def __repr__(self) -> str: attrs = [ @@ -82,24 +92,38 @@ def _copy(cls, client_status: Self, /) -> Self: @property def status(self) -> Status: + """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" return try_enum(Status, self._status) @property def raw_status(self) -> str: + """:class:`str`: The user's overall status as a string value.""" return self._status @property def mobile_status(self) -> Status: + """:class:`Status`: The user's status on a mobile device, if applicable.""" return try_enum(Status, self.mobile or 'offline') @property def desktop_status(self) -> Status: + """:class:`Status`: The user's status on the desktop client, if applicable.""" return try_enum(Status, self.desktop or 'offline') @property def web_status(self) -> Status: + """:class:`Status`: The user's status on the web client, if applicable.""" return try_enum(Status, self.web or 'offline') + def is_on_mobile(self) -> bool: + """A helper function that determines if a user is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self.mobile is not None + class RawPresenceUpdateEvent(_RawReprMixin): """Represents the payload for a :func:`on_raw_presence_update` event. @@ -112,6 +136,10 @@ class RawPresenceUpdateEvent(_RawReprMixin): The ID of the user that triggered the presence update. guild_id: Optional[:class:`int`] The guild ID for the users presence update. Could be ``None``. + guild: Optional[:class:`~.Guild`] + The guild associated with the presence update and user. Could be ``None``. + client_status: :class:`~.ClientStatus` + The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. """ __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', '_activities') @@ -119,8 +147,7 @@ class RawPresenceUpdateEvent(_RawReprMixin): def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: self.user_id: int = int(data["user"]["id"]) - self.client_status: _ClientStatus = _ClientStatus() - self.client_status._update(data["status"], data["client_status"]) + self.client_status: ClientStatus = ClientStatus(status=data["status"], data=data["client_status"]) self._activities: Tuple[ActivityTypes, ...] | None = None self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') @@ -140,40 +167,16 @@ def activities(self) -> Tuple[ActivityTypes, ...]: than ``128`` characters. See :issue:`1738` for more information. """ return self._activities or () + + @classmethod + def _copy(cls, other: RawPresenceUpdateEvent) -> Self: + new = cls.__new__(cls) + + new.user_id = other.user_id + new.client_status = ClientStatus._copy(other.client_status) + new._activities = other._activities + new.guild_id = other.guild_id + new.guild = other.guild + + return new - @property - def status(self) -> Status: - """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return self.client_status.status - - @property - def raw_status(self) -> str: - """:class:`str`: The member's overall status as a string value. - - .. versionadded:: 1.5 - """ - return self.client_status._status - - @property - def mobile_status(self) -> Status: - """:class:`Status`: The member's status on a mobile device, if applicable.""" - return self.client_status.mobile_status - - @property - def desktop_status(self) -> Status: - """:class:`Status`: The member's status on the desktop client, if applicable.""" - return self.client_status.desktop_status - - @property - def web_status(self) -> Status: - """:class:`Status`: The member's status on the web client, if applicable.""" - return self.client_status.web_status - - def is_on_mobile(self) -> bool: - """A helper function that determines if a member is active on a mobile device. - - Returns - ------- - :class:`bool` - """ - return self.client_status.mobile is not None From e8d1a10de00d0b65ee27e42c45b73ed1c49e06ae Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:47:37 +1000 Subject: [PATCH 11/37] Add missing raw from parse_guild_members_chunk --- discord/state.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/state.py b/discord/state.py index ea1758b0fc83..5625ccf4a288 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1411,11 +1411,13 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: if presences: member_dict: Dict[Snowflake, Member] = {str(member.id): member for member in members} for presence in presences: + raw = RawPresenceUpdateEvent(data=presence, state=self) user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) + if member is not None: - member._presence_update(presence, user) + member._presence_update(presence, raw, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) From 9c2a4f1a89b3c4ffaaec49e21e51bda50aeb4efa Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:48:28 +1000 Subject: [PATCH 12/37] Run black --- discord/member.py | 6 +++--- discord/presences.py | 16 ++++++++-------- discord/state.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/discord/member.py b/discord/member.py index e5178f77698f..a8fcaa0c1a47 100644 --- a/discord/member.py +++ b/discord/member.py @@ -478,12 +478,12 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: ) # Signal to dispatch on_user_update return to_return, u - + @property def client_status(self) -> ClientStatus: - """:class:`ClientStatus`: Model which holds information about the status of the + """:class:`ClientStatus`: Model which holds information about the status of the member on various clients. - + .. versionadded:: 2.5 """ return self._client_status diff --git a/discord/presences.py b/discord/presences.py index 37e39617299b..532ec8f6897c 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -43,14 +43,15 @@ class ClientStatus: """The :class:`ClientStatus` model which holds information about the status of the user on various clients. - + .. note:: - - You usually wouldn't instantiate this class manually, instead it should be available on members with + + You usually wouldn't instantiate this class manually, instead it should be available on members with :attr:`Member.client_status` and on the :class:`RawPresenceUpdateEvent` model. - + .. versionadded:: 2.5 """ + __slots__ = ('_status', 'desktop', 'mobile', 'web') def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None: @@ -167,16 +168,15 @@ def activities(self) -> Tuple[ActivityTypes, ...]: than ``128`` characters. See :issue:`1738` for more information. """ return self._activities or () - + @classmethod def _copy(cls, other: RawPresenceUpdateEvent) -> Self: new = cls.__new__(cls) - + new.user_id = other.user_id new.client_status = ClientStatus._copy(other.client_status) new._activities = other._activities new.guild_id = other.guild_id new.guild = other.guild - - return new + return new diff --git a/discord/state.py b/discord/state.py index 5625ccf4a288..6b06c2ec6dbb 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1415,7 +1415,7 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) - + if member is not None: member._presence_update(presence, raw, user) From db2b038c968656a35dc68c9f8025635a54530b63 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:51:56 +1000 Subject: [PATCH 13/37] Add ClientStatus to all --- discord/presences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index 532ec8f6897c..0166fbe916da 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -38,7 +38,7 @@ from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate -__all__ = ('RawPresenceUpdateEvent',) +__all__ = ('RawPresenceUpdateEvent', 'ClientStatus') class ClientStatus: From 887a3a02c5d55686314d17e72a1acc4ad8f4face Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:54:30 +1000 Subject: [PATCH 14/37] ClientStatus docs --- discord/presences.py | 2 +- docs/api.rst | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index 0166fbe916da..509463534b66 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -46,7 +46,7 @@ class ClientStatus: .. note:: - You usually wouldn't instantiate this class manually, instead it should be available on members with + You shouldn't instantiate this class manually, instead it should be available on members with :attr:`Member.client_status` and on the :class:`RawPresenceUpdateEvent` model. .. versionadded:: 2.5 diff --git a/docs/api.rst b/docs/api.rst index e14e57593ee9..392641f97c2b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5418,6 +5418,14 @@ MessageSnapshot .. autoclass:: MessageSnapshot :members: +ClientStatus +~~~~~~~~~~~~ + +.. attributetable:: ClientStatus + +.. autoclass:: ClientStatus + :members: + Data Classes -------------- From 1552846fed1966515f321b2fc5086c0fb7d86fab Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:37:26 +1000 Subject: [PATCH 15/37] Change flag name --- discord/client.py | 2 +- discord/state.py | 2 +- docs/api.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/client.py b/discord/client.py index 139b5ef94bb0..b9df23d28bdc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -237,7 +237,7 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 - enable_raw_presence_event: :class:`bool` + enable_raw_presences: :class:`bool` Whether to manually enable or disable the :func:`on_raw_presence_update` event. Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. diff --git a/discord/state.py b/discord/state.py index 6b06c2ec6dbb..5fc273bc9133 100644 --- a/discord/state.py +++ b/discord/state.py @@ -262,7 +262,7 @@ def __init__( if not intents.members or cache_flags._empty: self.store_user = self.store_user_no_intents - self.raw_presence_flag: bool = options.get('enable_raw_presence_event', utils.MISSING) + self.raw_presence_flag: bool = options.get('enable_raw_presences', utils.MISSING) if self.raw_presence_flag is utils.MISSING: self.raw_presence_flag = not intents.members and intents.presences diff --git a/docs/api.rst b/docs/api.rst index 392641f97c2b..b957919c2c60 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -930,7 +930,7 @@ Members By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` is disabled. - You can maually override this behaviour by setting the **enable_raw_presence_event** flag in the :class:`Client`, + You can maually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, however :attr:`Intents.presences` is always required for this event to work. .. versionadded:: 2.5 From ccad7970551f03237a5587df6d539018db021dc7 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:47:09 +1000 Subject: [PATCH 16/37] Small cleanup to state --- discord/state.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discord/state.py b/discord/state.py index 5fc273bc9133..2903e8cd0dfd 100644 --- a/discord/state.py +++ b/discord/state.py @@ -817,7 +817,6 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) return - user = data['user'] member = raw.guild.get_member(raw.user_id) if member is None: @@ -825,7 +824,7 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: return old_member = Member._copy(member) - user_update = member._presence_update(data, raw=raw, user=user) + user_update = member._presence_update(data, raw=raw, user=data['user']) if user_update: self.dispatch('user_update', user_update[0], user_update[1]) From 5f2027050be3adce038558aa24a1d6883f9a6162 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:09:10 +1000 Subject: [PATCH 17/37] Change some docs --- discord/presences.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/presences.py b/discord/presences.py index 509463534b66..07031905d749 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -42,7 +42,8 @@ class ClientStatus: - """The :class:`ClientStatus` model which holds information about the status of the user on various clients. + """Represents the :ddocs:`Client Status Object` from Discord, + which holds information about the status of the user on various clients/platforms, with additional helpers. .. note:: @@ -137,9 +138,9 @@ class RawPresenceUpdateEvent(_RawReprMixin): The ID of the user that triggered the presence update. guild_id: Optional[:class:`int`] The guild ID for the users presence update. Could be ``None``. - guild: Optional[:class:`~.Guild`] + guild: Optional[:class:`Guild`] The guild associated with the presence update and user. Could be ``None``. - client_status: :class:`~.ClientStatus` + client_status: :class:`ClientStatus` The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. """ From 01d249f5ddff0ebdf6be462dd2f4b5d9808d3263 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:10:03 +1000 Subject: [PATCH 18/37] Run black --- discord/presences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index 07031905d749..9558f26c37ab 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -42,7 +42,7 @@ class ClientStatus: - """Represents the :ddocs:`Client Status Object` from Discord, + """Represents the :ddocs:`Client Status Object` from Discord, which holds information about the status of the user on various clients/platforms, with additional helpers. .. note:: From 17b760bc2cf0fd524977c52a768896cfc945cfff Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:17:56 +1000 Subject: [PATCH 19/37] Fix some docs --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index b957919c2c60..0ad9464ea2f4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -930,12 +930,12 @@ Members By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` is disabled. - You can maually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, + You can manually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, however :attr:`Intents.presences` is always required for this event to work. .. versionadded:: 2.5 - :param payload: The raw presence update event payload class. + :param payload: The raw presence update event model. :type payload: :class:`RawPresenceUpdateEvent` From 158122d6b162ca529e822c640102e4d40bb25e5c Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:08:44 +1000 Subject: [PATCH 20/37] Un-property client_status --- discord/member.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/discord/member.py b/discord/member.py index a8fcaa0c1a47..cf3979cf21b3 100644 --- a/discord/member.py +++ b/discord/member.py @@ -264,6 +264,10 @@ class Member(discord.abc.Messageable, _UserTag): This will be set to ``None`` if the user is not timed out. .. versionadded:: 2.0 + client_status: :class:`ClientStatus` + Model which holds information about the status of the member on various clients/platforms via presence updates. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -276,7 +280,7 @@ class Member(discord.abc.Messageable, _UserTag): 'nick', 'timed_out_until', '_permissions', - '_client_status', + 'client_status', '_user', '_state', '_avatar', @@ -312,7 +316,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at')) self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since')) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles'])) - self._client_status: ClientStatus = ClientStatus() + self.client_status: ClientStatus = ClientStatus() self.activities: Tuple[ActivityTypes, ...] = () self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) @@ -388,7 +392,7 @@ def _copy(cls, member: Self) -> Self: self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self._client_status = ClientStatus._copy(member._client_status) + self.client_status = ClientStatus._copy(member.client_status) self.guild = member.guild self.nick = member.nick self.pending = member.pending @@ -438,7 +442,7 @@ def _presence_update( raw._create_activities(data, self._state) self.activities = raw.activities - self._client_status = raw.client_status + self.client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) @@ -479,19 +483,10 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: # Signal to dispatch on_user_update return to_return, u - @property - def client_status(self) -> ClientStatus: - """:class:`ClientStatus`: Model which holds information about the status of the - member on various clients. - - .. versionadded:: 2.5 - """ - return self._client_status - @property def status(self) -> Status: """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return self._client_status.status + return self.client_status.status @property def raw_status(self) -> str: @@ -499,27 +494,27 @@ def raw_status(self) -> str: .. versionadded:: 1.5 """ - return self._client_status._status + return self.client_status._status @status.setter def status(self, value: Status) -> None: # internal use only - self._client_status._status = str(value) + self.client_status._status = str(value) @property def mobile_status(self) -> Status: """:class:`Status`: The member's status on a mobile device, if applicable.""" - return self._client_status.mobile_status + return self.client_status.mobile_status @property def desktop_status(self) -> Status: """:class:`Status`: The member's status on the desktop client, if applicable.""" - return self._client_status.desktop_status + return self.client_status.desktop_status @property def web_status(self) -> Status: """:class:`Status`: The member's status on the web client, if applicable.""" - return self._client_status.web_status + return self.client_status.web_status def is_on_mobile(self) -> bool: """A helper function that determines if a member is active on a mobile device. @@ -528,7 +523,7 @@ def is_on_mobile(self) -> bool: ------- :class:`bool` """ - return self._client_status.mobile is not None + return self.client_status.mobile is not None @property def colour(self) -> Colour: From a292c0243aa06fc76ab3e1371652844f9bea8ca1 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:09:22 +1000 Subject: [PATCH 21/37] Update docs --- discord/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/client.py b/discord/client.py index b9df23d28bdc..06f380901b28 100644 --- a/discord/client.py +++ b/discord/client.py @@ -241,9 +241,9 @@ class Client: Whether to manually enable or disable the :func:`on_raw_presence_update` event. Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. - - By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` - is disabled, otherwise set to ``False``. + + By default, this flag is set to True only when :attr:Intents.presences is enabled and :attr:Intents.members + is disabled, otherwise it's set to False. .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` From b91c1fd1006cccb43bf58b297e5863229692ef36 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:12:01 +1000 Subject: [PATCH 22/37] Remove copy in Member._copy --- discord/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/member.py b/discord/member.py index cf3979cf21b3..7883cd88fcbb 100644 --- a/discord/member.py +++ b/discord/member.py @@ -392,7 +392,7 @@ def _copy(cls, member: Self) -> Self: self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self.client_status = ClientStatus._copy(member.client_status) + self.client_status = member.client_status self.guild = member.guild self.nick = member.nick self.pending = member.pending From 863836f0520c973ddac042292672fa91b6d74a05 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:14:58 +1000 Subject: [PATCH 23/37] Run black --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 06f380901b28..804c95701cb4 100644 --- a/discord/client.py +++ b/discord/client.py @@ -241,7 +241,7 @@ class Client: Whether to manually enable or disable the :func:`on_raw_presence_update` event. Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. - + By default, this flag is set to True only when :attr:Intents.presences is enabled and :attr:Intents.members is disabled, otherwise it's set to False. From 53802e6ff98e5465759e9168ee06bf37bd8ff286 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:23:25 +1000 Subject: [PATCH 24/37] Remove raw copy --- discord/presences.py | 12 ------------ discord/state.py | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/discord/presences.py b/discord/presences.py index 9558f26c37ab..f4fcd8d1ef9e 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -169,15 +169,3 @@ def activities(self) -> Tuple[ActivityTypes, ...]: than ``128`` characters. See :issue:`1738` for more information. """ return self._activities or () - - @classmethod - def _copy(cls, other: RawPresenceUpdateEvent) -> Self: - new = cls.__new__(cls) - - new.user_id = other.user_id - new.client_status = ClientStatus._copy(other.client_status) - new._activities = other._activities - new.guild_id = other.guild_id - new.guild = other.guild - - return new diff --git a/discord/state.py b/discord/state.py index 2903e8cd0dfd..d2f95d9a228e 100644 --- a/discord/state.py +++ b/discord/state.py @@ -811,7 +811,7 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: if self.raw_presence_flag: raw._create_activities(data, self) - self.dispatch('raw_presence_update', RawPresenceUpdateEvent._copy(raw)) + self.dispatch('raw_presence_update', raw) if raw.guild is None: _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) From eeb349c18c6a21875233d942b0ee93fa61bd9829 Mon Sep 17 00:00:00 2001 From: Eviee Py <29671945+EvieePy@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:35:42 +1000 Subject: [PATCH 25/37] Use old style Union --- discord/presences.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/presences.py b/discord/presences.py index f4fcd8d1ef9e..692cd0cb5379 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple, Union from .activity import create_activity from .enums import Status, try_enum @@ -150,10 +150,10 @@ def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> No self.user_id: int = int(data["user"]["id"]) self.client_status: ClientStatus = ClientStatus(status=data["status"], data=data["client_status"]) - self._activities: Tuple[ActivityTypes, ...] | None = None + self._activities: Union[Tuple[ActivityTypes, ...], None] = None self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') - self.guild: Guild | None = state._get_guild(self.guild_id) + self.guild: Union[Guild, None] = state._get_guild(self.guild_id) def _create_activities(self, data: PartialPresenceUpdate, state: ConnectionState) -> None: self._activities = tuple(create_activity(d, state) for d in data['activities']) From 430d8e6d1af3aabc58b77c96cfaef50c08d50822 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:27:18 +1000 Subject: [PATCH 26/37] Update docs --- discord/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index 804c95701cb4..b9b0b20735dc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -242,8 +242,8 @@ class Client: Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. - By default, this flag is set to True only when :attr:Intents.presences is enabled and :attr:Intents.members - is disabled, otherwise it's set to False. + By default, this flag is set to ``True`` only when :attr:Intents.presences is enabled and :attr:Intents.members + is disabled, otherwise it's set to ``False``. .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` From abfd31b3c75cc3427050f4a10f5d9543cc1e77e7 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:32:53 +1000 Subject: [PATCH 27/37] Add original presence update function for chunking --- discord/guild.py | 2 +- discord/member.py | 8 +++++++- discord/state.py | 3 +-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index d8291405b86c..fa7bde663865 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -541,7 +541,7 @@ def _from_data(self, guild: GuildPayload) -> None: member = self.get_member(raw.user_id) if member is not None: - member._presence_update(presence, raw, empty_tuple) # type: ignore + member._perf_presence_update(presence, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] diff --git a/discord/member.py b/discord/member.py index 7883cd88fcbb..08a316f0478f 100644 --- a/discord/member.py +++ b/discord/member.py @@ -43,6 +43,7 @@ from .object import Object from .flags import MemberFlags from .presences import ClientStatus +from .activity import create_activity __all__ = ( 'VoiceState', @@ -447,7 +448,12 @@ def _presence_update( if len(user) > 1: return self._update_inner_user(user) - return None + def _perf_presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: + self.activities = tuple(create_activity(d, self._state) for d in data['activities']) + self.client_status._update(data['status'], data['client_status']) + + if len(user) > 1: + return self._update_inner_user(user) def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user diff --git a/discord/state.py b/discord/state.py index d2f95d9a228e..a682b188c677 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1410,13 +1410,12 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: if presences: member_dict: Dict[Snowflake, Member] = {str(member.id): member for member in members} for presence in presences: - raw = RawPresenceUpdateEvent(data=presence, state=self) user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) if member is not None: - member._presence_update(presence, raw, user) + member._perf_presence_update(presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) From 39708ef16283ff2bea72c93486eccc99f379d97b Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:37:36 +1000 Subject: [PATCH 28/37] Remove raw object creation guild.py --- discord/guild.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index fa7bde663865..973f13e428a0 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -537,8 +537,8 @@ def _from_data(self, guild: GuildPayload) -> None: empty_tuple = () for presence in guild.get('presences', []): - raw = RawPresenceUpdateEvent(data=presence, state=self._state) - member = self.get_member(raw.user_id) + user_id = int(presence['user']['id']) + member = self.get_member(user_id) if member is not None: member._perf_presence_update(presence, empty_tuple) # type: ignore From b0f4330acea1671f04e45186227fcbba4f492390 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 02:05:52 +1000 Subject: [PATCH 29/37] Remove unused import --- discord/guild.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 973f13e428a0..222b2a768551 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,7 +95,6 @@ from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound -from .presences import RawPresenceUpdateEvent __all__ = ( From 80bb10e808399d83f50b20d7078ba3abf804a43d Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 02:29:52 +1000 Subject: [PATCH 30/37] Docs - Add missing backticks --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index b9b0b20735dc..bb381eff5057 100644 --- a/discord/client.py +++ b/discord/client.py @@ -242,7 +242,7 @@ class Client: Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. - By default, this flag is set to ``True`` only when :attr:Intents.presences is enabled and :attr:Intents.members + By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` is disabled, otherwise it's set to ``False``. .. versionadded:: 2.5 From c572c36fee670b134f9c9334aa8d089235d2080a Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Sun, 22 Dec 2024 03:02:32 +1000 Subject: [PATCH 31/37] Apply requested changes. --- discord/member.py | 2 +- discord/presences.py | 11 +++-------- docs/api.rst | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/discord/member.py b/discord/member.py index 08a316f0478f..1c7360baf5d1 100644 --- a/discord/member.py +++ b/discord/member.py @@ -529,7 +529,7 @@ def is_on_mobile(self) -> bool: ------- :class:`bool` """ - return self.client_status.mobile is not None + return self.client_status.is_on_mobile() @property def colour(self) -> Colour: diff --git a/discord/presences.py b/discord/presences.py index 692cd0cb5379..ce5eb5582cc3 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -42,14 +42,9 @@ class ClientStatus: - """Represents the :ddocs:`Client Status Object` from Discord, + """Represents the :ddocs:`Client Status Object ` from Discord, which holds information about the status of the user on various clients/platforms, with additional helpers. - .. note:: - - You shouldn't instantiate this class manually, instead it should be available on members with - :attr:`Member.client_status` and on the :class:`RawPresenceUpdateEvent` model. - .. versionadded:: 2.5 """ @@ -147,9 +142,9 @@ class RawPresenceUpdateEvent(_RawReprMixin): __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', '_activities') def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: - self.user_id: int = int(data["user"]["id"]) + self.user_id: int = int(data['user']['id']) - self.client_status: ClientStatus = ClientStatus(status=data["status"], data=data["client_status"]) + self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status']) self._activities: Union[Tuple[ActivityTypes, ...], None] = None self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') diff --git a/docs/api.rst b/docs/api.rst index 0ad9464ea2f4..8eb6a97e0284 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5423,7 +5423,7 @@ ClientStatus .. attributetable:: ClientStatus -.. autoclass:: ClientStatus +.. autoclass:: ClientStatus() :members: Data Classes From 151b5c4ecb630a3aeb7a5e6de54a04244207f2d0 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:26:45 +1000 Subject: [PATCH 32/37] Update discord/presences.py Co-authored-by: MCausc78 --- discord/presences.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/discord/presences.py b/discord/presences.py index ce5eb5582cc3..9f23ff4c8fb1 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -113,12 +113,7 @@ def web_status(self) -> Status: return try_enum(Status, self.web or 'offline') def is_on_mobile(self) -> bool: - """A helper function that determines if a user is active on a mobile device. - - Returns - ------- - :class:`bool` - """ + """:class:`bool`: A helper function that determines if a user is active on a mobile device.""" return self.mobile is not None From 60ab7151e8d6c443092df469f454308ed09b35d7 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:27:04 +1000 Subject: [PATCH 33/37] Update discord/presences.py Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/presences.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index 9f23ff4c8fb1..62768e6cb4cf 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -38,7 +38,10 @@ from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate -__all__ = ('RawPresenceUpdateEvent', 'ClientStatus') +__all__ = ( + 'RawPresenceUpdateEvent', + 'ClientStatus', +) class ClientStatus: From 1607bdfe28964c4253d320321d2a61c4e32e0b21 Mon Sep 17 00:00:00 2001 From: Mysty <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:27:42 +1000 Subject: [PATCH 34/37] Update discord/presences.py Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com> --- discord/presences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index 62768e6cb4cf..baf010f44923 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -146,7 +146,7 @@ def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> No self._activities: Union[Tuple[ActivityTypes, ...], None] = None self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') - self.guild: Union[Guild, None] = state._get_guild(self.guild_id) + self.guild: Optional[Guild] = state._get_guild(self.guild_id) def _create_activities(self, data: PartialPresenceUpdate, state: ConnectionState) -> None: self._activities = tuple(create_activity(d, state) for d in data['activities']) From c1ba7f645025928b102e1ee3fe9de1606ee69f75 Mon Sep 17 00:00:00 2001 From: EvieePy <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:10:23 +1000 Subject: [PATCH 35/37] Implement requested changes --- discord/guild.py | 8 ++++---- discord/member.py | 17 ++--------------- discord/presences.py | 24 +++++------------------- discord/state.py | 6 +++--- 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 44604f6c46b0..b7e53f0c79ba 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,7 +95,7 @@ from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound - +from .presences import RawPresenceUpdateEvent __all__ = ( 'Guild', @@ -653,11 +653,11 @@ def _from_data(self, guild: GuildPayload) -> None: empty_tuple = () for presence in guild.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state) + member = self.get_member(raw_presence.user_id) if member is not None: - member._perf_presence_update(presence, empty_tuple) # type: ignore + member._presence_update(raw_presence, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] diff --git a/discord/member.py b/discord/member.py index fbf2e309af23..2de8fbfc1f34 100644 --- a/discord/member.py +++ b/discord/member.py @@ -43,7 +43,6 @@ from .object import Object from .flags import MemberFlags from .presences import ClientStatus -from .activity import create_activity __all__ = ( 'VoiceState', @@ -65,7 +64,7 @@ Member as MemberPayload, UserWithMember as UserWithMemberPayload, ) - from .types.gateway import GuildMemberUpdateEvent, PartialPresenceUpdate + from .types.gateway import GuildMemberUpdateEvent from .types.user import User as UserPayload, AvatarDecorationData from .abc import Snowflake from .state import ConnectionState @@ -436,25 +435,13 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') - def _presence_update( - self, data: PartialPresenceUpdate, raw: RawPresenceUpdateEvent, user: UserPayload - ) -> Optional[Tuple[User, User]]: - if raw._activities is None: - raw._create_activities(data, self._state) - + def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]: self.activities = raw.activities self.client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) - def _perf_presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: - self.activities = tuple(create_activity(d, self._state) for d in data['activities']) - self.client_status._update(data['status'], data['client_status']) - - if len(user) > 1: - return self._update_inner_user(user) - def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user original = ( diff --git a/discord/presences.py b/discord/presences.py index ce5eb5582cc3..ad7dc9973966 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -137,30 +137,16 @@ class RawPresenceUpdateEvent(_RawReprMixin): The guild associated with the presence update and user. Could be ``None``. client_status: :class:`ClientStatus` The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. + activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] + The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information. """ - __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', '_activities') + __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities') def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: self.user_id: int = int(data['user']['id']) - self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status']) - self._activities: Union[Tuple[ActivityTypes, ...], None] = None - + self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities']) self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') self.guild: Union[Guild, None] = state._get_guild(self.guild_id) - - def _create_activities(self, data: PartialPresenceUpdate, state: ConnectionState) -> None: - self._activities = tuple(create_activity(d, state) for d in data['activities']) - - @property - def activities(self) -> Tuple[ActivityTypes, ...]: - """Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]: The activities the user is currently doing. - - .. note:: - - Due to a Discord API limitation, a user's Spotify activity may not appear - if they are listening to a song with a title longer - than ``128`` characters. See :issue:`1738` for more information. - """ - return self._activities or () diff --git a/discord/state.py b/discord/state.py index a542d5689b2b..ae34ce138841 100644 --- a/discord/state.py +++ b/discord/state.py @@ -835,7 +835,6 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: raw = RawPresenceUpdateEvent(data=data, state=self) if self.raw_presence_flag: - raw._create_activities(data, self) self.dispatch('raw_presence_update', raw) if raw.guild is None: @@ -849,7 +848,7 @@ def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: return old_member = Member._copy(member) - user_update = member._presence_update(data, raw=raw, user=data['user']) + user_update = member._presence_update(raw=raw, user=data['user']) if user_update: self.dispatch('user_update', user_update[0], user_update[1]) @@ -1439,8 +1438,9 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: member_id = user['id'] member = member_dict.get(member_id) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self) if member is not None: - member._perf_presence_update(presence, user) + member._presence_update(raw_presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) From 8a5eee9090fcb197eebaac2455383ab7c074d36c Mon Sep 17 00:00:00 2001 From: EvieePy <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:17:27 +1000 Subject: [PATCH 36/37] Remove unused import --- discord/presences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/presences.py b/discord/presences.py index cdfab525f5a3..7fec2a09dfcc 100644 --- a/discord/presences.py +++ b/discord/presences.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Tuple from .activity import create_activity from .enums import Status, try_enum From f28bff6a1d359b41583d3a9d911bf2d533b181c1 Mon Sep 17 00:00:00 2001 From: EvieePy <29671945+EvieePy@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:45:31 +1000 Subject: [PATCH 37/37] Implement changes in state.py --- discord/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/state.py b/discord/state.py index ae34ce138841..b1409f809100 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1438,8 +1438,8 @@ def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None: member_id = user['id'] member = member_dict.get(member_id) - raw_presence = RawPresenceUpdateEvent(data=presence, state=self) if member is not None: + raw_presence = RawPresenceUpdateEvent(data=presence, state=self) member._presence_update(raw_presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count')