Skip to content

Commit 418a791

Browse files
authored
Add raw presence update evemt
1 parent afbbc07 commit 418a791

File tree

10 files changed

+262
-86
lines changed

10 files changed

+262
-86
lines changed

discord/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from .poll import *
7373
from .soundboard import *
7474
from .subscription import *
75+
from .presences import *
7576

7677

7778
class VersionInfo(NamedTuple):

discord/client.py

+9
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ class Client:
237237
To enable these events, this must be set to ``True``. Defaults to ``False``.
238238
239239
.. versionadded:: 2.0
240+
enable_raw_presences: :class:`bool`
241+
Whether to manually enable or disable the :func:`on_raw_presence_update` event.
242+
243+
Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled.
244+
245+
By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members`
246+
is disabled, otherwise it's set to ``False``.
247+
248+
.. versionadded:: 2.5
240249
http_trace: :class:`aiohttp.TraceConfig`
241250
The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``.
242251
This allows you to check requests the library is using. For more information, check the

discord/guild.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
9696
from .partial_emoji import _EmojiTag, PartialEmoji
9797
from .soundboard import SoundboardSound
98-
98+
from .presences import RawPresenceUpdateEvent
9999

100100
__all__ = (
101101
'Guild',
@@ -653,10 +653,11 @@ def _from_data(self, guild: GuildPayload) -> None:
653653

654654
empty_tuple = ()
655655
for presence in guild.get('presences', []):
656-
user_id = int(presence['user']['id'])
657-
member = self.get_member(user_id)
656+
raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state)
657+
member = self.get_member(raw_presence.user_id)
658+
658659
if member is not None:
659-
member._presence_update(presence, empty_tuple) # type: ignore
660+
member._presence_update(raw_presence, empty_tuple) # type: ignore
660661

661662
if 'threads' in guild:
662663
threads = guild['threads']

discord/member.py

+27-61
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@
3636
from .asset import Asset
3737
from .utils import MISSING
3838
from .user import BaseUser, ClientUser, User, _UserTag
39-
from .activity import create_activity, ActivityTypes
4039
from .permissions import Permissions
41-
from .enums import Status, try_enum
40+
from .enums import Status
4241
from .errors import ClientException
4342
from .colour import Colour
4443
from .object import Object
4544
from .flags import MemberFlags
45+
from .presences import ClientStatus
4646

4747
__all__ = (
4848
'VoiceState',
@@ -57,10 +57,8 @@
5757
from .channel import DMChannel, VoiceChannel, StageChannel
5858
from .flags import PublicUserFlags
5959
from .guild import Guild
60-
from .types.activity import (
61-
ClientStatus as ClientStatusPayload,
62-
PartialPresenceUpdate,
63-
)
60+
from .activity import ActivityTypes
61+
from .presences import RawPresenceUpdateEvent
6462
from .types.member import (
6563
MemberWithUser as MemberWithUserPayload,
6664
Member as MemberPayload,
@@ -168,46 +166,6 @@ def __repr__(self) -> str:
168166
return f'<{self.__class__.__name__} {inner}>'
169167

170168

171-
class _ClientStatus:
172-
__slots__ = ('_status', 'desktop', 'mobile', 'web')
173-
174-
def __init__(self):
175-
self._status: str = 'offline'
176-
177-
self.desktop: Optional[str] = None
178-
self.mobile: Optional[str] = None
179-
self.web: Optional[str] = None
180-
181-
def __repr__(self) -> str:
182-
attrs = [
183-
('_status', self._status),
184-
('desktop', self.desktop),
185-
('mobile', self.mobile),
186-
('web', self.web),
187-
]
188-
inner = ' '.join('%s=%r' % t for t in attrs)
189-
return f'<{self.__class__.__name__} {inner}>'
190-
191-
def _update(self, status: str, data: ClientStatusPayload, /) -> None:
192-
self._status = status
193-
194-
self.desktop = data.get('desktop')
195-
self.mobile = data.get('mobile')
196-
self.web = data.get('web')
197-
198-
@classmethod
199-
def _copy(cls, client_status: Self, /) -> Self:
200-
self = cls.__new__(cls) # bypass __init__
201-
202-
self._status = client_status._status
203-
204-
self.desktop = client_status.desktop
205-
self.mobile = client_status.mobile
206-
self.web = client_status.web
207-
208-
return self
209-
210-
211169
def flatten_user(cls: T) -> T:
212170
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
213171
# ignore private/special methods
@@ -306,6 +264,10 @@ class Member(discord.abc.Messageable, _UserTag):
306264
This will be set to ``None`` or a time in the past if the user is not timed out.
307265
308266
.. versionadded:: 2.0
267+
client_status: :class:`ClientStatus`
268+
Model which holds information about the status of the member on various clients/platforms via presence updates.
269+
270+
.. versionadded:: 2.5
309271
"""
310272

311273
__slots__ = (
@@ -318,7 +280,7 @@ class Member(discord.abc.Messageable, _UserTag):
318280
'nick',
319281
'timed_out_until',
320282
'_permissions',
321-
'_client_status',
283+
'client_status',
322284
'_user',
323285
'_state',
324286
'_avatar',
@@ -354,7 +316,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti
354316
self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at'))
355317
self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since'))
356318
self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles']))
357-
self._client_status: _ClientStatus = _ClientStatus()
319+
self.client_status: ClientStatus = ClientStatus()
358320
self.activities: Tuple[ActivityTypes, ...] = ()
359321
self.nick: Optional[str] = data.get('nick', None)
360322
self.pending: bool = data.get('pending', False)
@@ -430,7 +392,7 @@ def _copy(cls, member: Self) -> Self:
430392
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
431393
self.joined_at = member.joined_at
432394
self.premium_since = member.premium_since
433-
self._client_status = _ClientStatus._copy(member._client_status)
395+
self.client_status = member.client_status
434396
self.guild = member.guild
435397
self.nick = member.nick
436398
self.pending = member.pending
@@ -473,13 +435,12 @@ def _update(self, data: GuildMemberUpdateEvent) -> None:
473435
self._flags = data.get('flags', 0)
474436
self._avatar_decoration_data = data.get('avatar_decoration_data')
475437

476-
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
477-
self.activities = tuple(create_activity(d, self._state) for d in data['activities'])
478-
self._client_status._update(data['status'], data['client_status'])
438+
def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]:
439+
self.activities = raw.activities
440+
self.client_status = raw.client_status
479441

480442
if len(user) > 1:
481443
return self._update_inner_user(user)
482-
return None
483444

484445
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
485446
u = self._user
@@ -518,39 +479,44 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
518479
@property
519480
def status(self) -> Status:
520481
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
521-
return try_enum(Status, self._client_status._status)
482+
return self.client_status.status
522483

523484
@property
524485
def raw_status(self) -> str:
525486
""":class:`str`: The member's overall status as a string value.
526487
527488
.. versionadded:: 1.5
528489
"""
529-
return self._client_status._status
490+
return self.client_status._status
530491

531492
@status.setter
532493
def status(self, value: Status) -> None:
533494
# internal use only
534-
self._client_status._status = str(value)
495+
self.client_status._status = str(value)
535496

536497
@property
537498
def mobile_status(self) -> Status:
538499
""":class:`Status`: The member's status on a mobile device, if applicable."""
539-
return try_enum(Status, self._client_status.mobile or 'offline')
500+
return self.client_status.mobile_status
540501

541502
@property
542503
def desktop_status(self) -> Status:
543504
""":class:`Status`: The member's status on the desktop client, if applicable."""
544-
return try_enum(Status, self._client_status.desktop or 'offline')
505+
return self.client_status.desktop_status
545506

546507
@property
547508
def web_status(self) -> Status:
548509
""":class:`Status`: The member's status on the web client, if applicable."""
549-
return try_enum(Status, self._client_status.web or 'offline')
510+
return self.client_status.web_status
550511

551512
def is_on_mobile(self) -> bool:
552-
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
553-
return self._client_status.mobile is not None
513+
"""A helper function that determines if a member is active on a mobile device.
514+
515+
Returns
516+
-------
517+
:class:`bool`
518+
"""
519+
return self.client_status.is_on_mobile()
554520

555521
@property
556522
def colour(self) -> Colour:

discord/presences.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-present Rapptz
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
from __future__ import annotations
25+
26+
from typing import TYPE_CHECKING, Optional, Tuple
27+
28+
from .activity import create_activity
29+
from .enums import Status, try_enum
30+
from .utils import MISSING, _get_as_snowflake, _RawReprMixin
31+
32+
if TYPE_CHECKING:
33+
from typing_extensions import Self
34+
35+
from .activity import ActivityTypes
36+
from .guild import Guild
37+
from .state import ConnectionState
38+
from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate
39+
40+
41+
__all__ = (
42+
'RawPresenceUpdateEvent',
43+
'ClientStatus',
44+
)
45+
46+
47+
class ClientStatus:
48+
"""Represents the :ddocs:`Client Status Object <events/gateway-events#client-status-object>` from Discord,
49+
which holds information about the status of the user on various clients/platforms, with additional helpers.
50+
51+
.. versionadded:: 2.5
52+
"""
53+
54+
__slots__ = ('_status', 'desktop', 'mobile', 'web')
55+
56+
def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None:
57+
self._status: str = status or 'offline'
58+
59+
data = data or {}
60+
self.desktop: Optional[str] = data.get('desktop')
61+
self.mobile: Optional[str] = data.get('mobile')
62+
self.web: Optional[str] = data.get('web')
63+
64+
def __repr__(self) -> str:
65+
attrs = [
66+
('_status', self._status),
67+
('desktop', self.desktop),
68+
('mobile', self.mobile),
69+
('web', self.web),
70+
]
71+
inner = ' '.join('%s=%r' % t for t in attrs)
72+
return f'<{self.__class__.__name__} {inner}>'
73+
74+
def _update(self, status: str, data: ClientStatusPayload, /) -> None:
75+
self._status = status
76+
77+
self.desktop = data.get('desktop')
78+
self.mobile = data.get('mobile')
79+
self.web = data.get('web')
80+
81+
@classmethod
82+
def _copy(cls, client_status: Self, /) -> Self:
83+
self = cls.__new__(cls) # bypass __init__
84+
85+
self._status = client_status._status
86+
87+
self.desktop = client_status.desktop
88+
self.mobile = client_status.mobile
89+
self.web = client_status.web
90+
91+
return self
92+
93+
@property
94+
def status(self) -> Status:
95+
""":class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead."""
96+
return try_enum(Status, self._status)
97+
98+
@property
99+
def raw_status(self) -> str:
100+
""":class:`str`: The user's overall status as a string value."""
101+
return self._status
102+
103+
@property
104+
def mobile_status(self) -> Status:
105+
""":class:`Status`: The user's status on a mobile device, if applicable."""
106+
return try_enum(Status, self.mobile or 'offline')
107+
108+
@property
109+
def desktop_status(self) -> Status:
110+
""":class:`Status`: The user's status on the desktop client, if applicable."""
111+
return try_enum(Status, self.desktop or 'offline')
112+
113+
@property
114+
def web_status(self) -> Status:
115+
""":class:`Status`: The user's status on the web client, if applicable."""
116+
return try_enum(Status, self.web or 'offline')
117+
118+
def is_on_mobile(self) -> bool:
119+
""":class:`bool`: A helper function that determines if a user is active on a mobile device."""
120+
return self.mobile is not None
121+
122+
123+
class RawPresenceUpdateEvent(_RawReprMixin):
124+
"""Represents the payload for a :func:`on_raw_presence_update` event.
125+
126+
.. versionadded:: 2.5
127+
128+
Attributes
129+
----------
130+
user_id: :class:`int`
131+
The ID of the user that triggered the presence update.
132+
guild_id: Optional[:class:`int`]
133+
The guild ID for the users presence update. Could be ``None``.
134+
guild: Optional[:class:`Guild`]
135+
The guild associated with the presence update and user. Could be ``None``.
136+
client_status: :class:`ClientStatus`
137+
The :class:`~.ClientStatus` model which holds information about the status of the user on various clients.
138+
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
139+
The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear
140+
if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information.
141+
"""
142+
143+
__slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities')
144+
145+
def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None:
146+
self.user_id: int = int(data['user']['id'])
147+
self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status'])
148+
self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities'])
149+
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
150+
self.guild: Optional[Guild] = state._get_guild(self.guild_id)

0 commit comments

Comments
 (0)