Skip to content

Commit 0847b02

Browse files
authored
Overhaul presence syncing and afk/idle tracking (#663)
* Overhaul presence syncing and afk/idle tracking * Log initial presence as well * Fix duplicate custom status checking
1 parent 7265307 commit 0847b02

File tree

3 files changed

+137
-46
lines changed

3 files changed

+137
-46
lines changed

discord/client.py

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ class Client:
186186
A status to start your presence with upon logging on to Discord.
187187
activity: Optional[:class:`.BaseActivity`]
188188
An activity to start your presence with upon logging on to Discord.
189+
activities: List[:class:`.BaseActivity`]
190+
A list of activities to start your presence with upon logging on to Discord. Cannot be sent with ``activity``.
191+
192+
.. versionadded:: 2.0
193+
afk: :class:`bool`
194+
Whether to start your session as AFK. Defaults to ``False``.
195+
196+
.. versionadded:: 2.1
189197
allowed_mentions: Optional[:class:`AllowedMentions`]
190198
Control how the client handles mentions by default on every message sent.
191199
@@ -324,6 +332,7 @@ def _handle_connect(self) -> None:
324332
if status or activities:
325333
if status is None:
326334
status = getattr(state.settings, 'status', None) or Status.unknown
335+
_log.debug('Setting initial presence to %s %s', status, activities)
327336
self.loop.create_task(self.change_presence(activities=activities, status=status))
328337

329338
@property
@@ -684,11 +693,21 @@ async def on_internal_settings_update(self, old_settings: UserSettings, new_sett
684693
):
685694
return # Nothing changed
686695

696+
current_activity = None
697+
for activity in self.activities:
698+
if activity.type != ActivityType.custom:
699+
current_activity = activity
700+
break
701+
702+
if new_settings.status == self.client_status and new_settings.custom_activity == current_activity:
703+
return # Nothing changed
704+
687705
status = new_settings.status
688-
activities = [a for a in self.activities if a.type != ActivityType.custom]
706+
activities = [a for a in self.client_activities if a.type != ActivityType.custom]
689707
if new_settings.custom_activity is not None:
690708
activities.append(new_settings.custom_activity)
691709

710+
_log.debug('Syncing presence to %s %s', status, new_settings.custom_activity)
692711
await self.change_presence(status=status, activities=activities, edit_settings=False)
693712

694713
# Hooks
@@ -1230,6 +1249,32 @@ def client_activities(self) -> Tuple[ActivityTypes]:
12301249
activities = (activity,) if activity else activities
12311250
return activities or tuple()
12321251

1252+
def is_afk(self) -> bool:
1253+
""":class:`bool`: Indicates if the client is currently AFK.
1254+
1255+
This allows the Discord client to know how to handle push notifications
1256+
better for you in case you are away from your keyboard.
1257+
1258+
.. versionadded:: 2.1
1259+
"""
1260+
if self.ws:
1261+
return self.ws.afk
1262+
return False
1263+
1264+
@property
1265+
def idle_since(self) -> Optional[datetime]:
1266+
"""Optional[:class:`datetime.datetime`]: When the client went idle.
1267+
1268+
This indicates that you are truly idle and not just lying.
1269+
1270+
.. versionadded:: 2.1
1271+
"""
1272+
ws = self.ws
1273+
if ws is None or not ws.idle_since:
1274+
return None
1275+
1276+
return utils.parse_timestamp(ws.idle_since)
1277+
12331278
@property
12341279
def allowed_mentions(self) -> Optional[AllowedMentions]:
12351280
"""Optional[:class:`~discord.AllowedMentions`]: The allowed mention configuration.
@@ -1606,21 +1651,30 @@ async def on_ready():
16061651
async def change_presence(
16071652
self,
16081653
*,
1609-
activity: Optional[ActivityTypes] = None,
1610-
activities: Optional[List[ActivityTypes]] = None,
1611-
status: Optional[Status] = None,
1612-
afk: bool = False,
1654+
activity: Optional[ActivityTypes] = MISSING,
1655+
activities: List[ActivityTypes] = MISSING,
1656+
status: Status = MISSING,
1657+
afk: bool = MISSING,
1658+
idle_since: Optional[datetime] = MISSING,
16131659
edit_settings: bool = True,
16141660
) -> None:
16151661
"""|coro|
16161662
16171663
Changes the client's presence.
16181664
1665+
.. versionchanged:: 2.1
1666+
1667+
The default value for parameters is now the current value.
1668+
``None`` is no longer a valid value for most; you must explicitly
1669+
set it to the default value if you want to reset it.
1670+
16191671
.. versionchanged:: 2.0
1672+
16201673
Edits are no longer in place.
16211674
Added option to update settings.
16221675
16231676
.. versionchanged:: 2.0
1677+
16241678
This function will now raise :exc:`TypeError` instead of
16251679
``InvalidArgument``.
16261680
@@ -1636,55 +1690,79 @@ async def change_presence(
16361690
----------
16371691
activity: Optional[:class:`.BaseActivity`]
16381692
The activity being done. ``None`` if no activity is done.
1639-
activities: Optional[List[:class:`.BaseActivity`]]
1640-
A list of the activities being done. ``None`` if no activities
1641-
are done. Cannot be sent with ``activity``.
1642-
status: Optional[:class:`.Status`]
1643-
Indicates what status to change to. If ``None``, then
1644-
:attr:`.Status.online` is used.
1693+
activities: List[:class:`.BaseActivity`]
1694+
A list of the activities being done. Cannot be sent with ``activity``.
1695+
1696+
.. versionadded:: 2.0
1697+
status: :class:`.Status`
1698+
Indicates what status to change to.
16451699
afk: :class:`bool`
16461700
Indicates if you are going AFK. This allows the Discord
16471701
client to know how to handle push notifications better
1648-
for you in case you are actually idle and not lying.
1702+
for you in case you are away from your keyboard.
1703+
idle_since: Optional[:class:`datetime.datetime`]
1704+
When the client went idle. This indicates that you are
1705+
truly idle and not just lying.
16491706
edit_settings: :class:`bool`
1650-
Whether to update the settings with the new status and/or
1707+
Whether to update user settings with the new status and/or
16511708
custom activity. This will broadcast the change and cause
16521709
all connected (official) clients to change presence as well.
1710+
1711+
This should be set to ``False`` for idle changes.
1712+
16531713
Required for setting/editing ``expires_at`` for custom activities.
1654-
It's not recommended to change this, as setting it to ``False`` causes undefined behavior.
1714+
It's not recommended to change this, as setting it to ``False``
1715+
can cause undefined behavior.
16551716
16561717
Raises
16571718
------
16581719
TypeError
16591720
The ``activity`` parameter is not the proper type.
16601721
Both ``activity`` and ``activities`` were passed.
1722+
ValueError
1723+
More than one custom activity was passed.
16611724
"""
1662-
if activity and activities:
1725+
if activity is not MISSING and activities is not MISSING:
16631726
raise TypeError('Cannot pass both activity and activities')
1664-
activities = activities or activity and [activity]
1665-
if activities is None:
1666-
activities = []
16671727

1668-
if status is None:
1669-
status = Status.online
1670-
elif status is Status.offline:
1671-
status = Status.invisible
1728+
skip_activities = False
1729+
if activities is MISSING:
1730+
if activity is not MISSING:
1731+
activities = [activity] if activity else []
1732+
else:
1733+
activities = list(self.client_activities)
1734+
skip_activities = True
1735+
else:
1736+
activities = activities or []
16721737

1673-
await self.ws.change_presence(status=status, activities=activities, afk=afk)
1738+
skip_status = status is MISSING
1739+
if status is MISSING:
1740+
status = self.client_status
1741+
if status is Status.offline:
1742+
status = Status.invisible
16741743

1675-
if edit_settings:
1676-
custom_activity = None
1744+
if idle_since is MISSING:
1745+
since = self.ws.idle_since if self.ws else 0
1746+
else:
1747+
since = int(idle_since.timestamp() * 1000) if idle_since else 0
16771748

1749+
custom_activity = None
1750+
if not skip_activities:
16781751
for activity in activities:
16791752
if getattr(activity, 'type', None) is ActivityType.custom:
1753+
if custom_activity is not None:
1754+
raise ValueError('More than one custom activity was passed')
16801755
custom_activity = activity
16811756

1757+
await self.ws.change_presence(status=status, activities=activities, afk=afk, since=since)
1758+
1759+
if edit_settings and self.settings:
16821760
payload: Dict[str, Any] = {}
1683-
if status != getattr(self.settings, 'status', None):
1761+
if not skip_status and status != self.settings.status:
16841762
payload['status'] = status
1685-
if custom_activity != getattr(self.settings, 'custom_activity', None):
1763+
if not skip_activities and custom_activity != self.settings.custom_activity:
16861764
payload['custom_activity'] = custom_activity
1687-
if payload and self.settings:
1765+
if payload:
16881766
await self.settings.edit(**payload)
16891767

16901768
async def change_voice_state(

discord/gateway.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import traceback
3333
import zlib
3434

35-
from typing import Any, Callable, Coroutine, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar
35+
from typing import Any, Callable, Coroutine, Dict, List, TYPE_CHECKING, NamedTuple, Optional, Sequence, TypeVar
3636

3737
import aiohttp
3838
import yarl
@@ -334,6 +334,10 @@ def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.Abs
334334
self._close_code: Optional[int] = None
335335
self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter()
336336

337+
# Presence state tracking
338+
self.afk: bool = False
339+
self.idle_since: int = 0
340+
337341
@property
338342
def open(self) -> bool:
339343
return not self.socket.closed
@@ -395,6 +399,8 @@ async def from_client(
395399
ws._user_agent = client.http.user_agent
396400
ws._super_properties = client.http.super_properties
397401
ws._zlib_enabled = zlib
402+
ws.afk = client._connection._afk
403+
ws.idle_since = client._connection._idle_since
398404

399405
if client._enable_debug_events:
400406
ws.send = ws.debug_send
@@ -456,15 +462,13 @@ async def identify(self) -> None:
456462
# but that needs more testing...
457463
presence = {
458464
'status': 'unknown',
459-
'since': 0,
465+
'since': self.idle_since,
460466
'activities': [],
461-
'afk': False,
467+
'afk': self.afk,
462468
}
463469
existing = self._connection.current_session
464470
if existing is not None:
465471
presence['status'] = str(existing.status) if existing.status is not Status.offline else 'invisible'
466-
if existing.status == Status.idle:
467-
presence['since'] = int(time.time() * 1000)
468472
presence['activities'] = [a.to_dict() for a in existing.activities]
469473
# else:
470474
# presence['status'] = self._connection._status or 'unknown'
@@ -482,11 +486,12 @@ async def identify(self) -> None:
482486
'client_state': {
483487
'api_code_version': 0,
484488
'guild_versions': {},
485-
'highest_last_message_id': '0',
486-
'private_channels_version': '0',
487-
'read_state_version': 0,
488-
'user_guild_settings_version': -1,
489-
'user_settings_version': -1,
489+
# 'highest_last_message_id': '0',
490+
# 'initial_guild_id': None,
491+
# 'private_channels_version': '0',
492+
# 'read_state_version': 0,
493+
# 'user_guild_settings_version': -1,
494+
# 'user_settings_version': -1,
490495
},
491496
},
492497
}
@@ -700,7 +705,7 @@ async def send_heartbeat(self, data: Any) -> None:
700705
async def change_presence(
701706
self,
702707
*,
703-
activities: Optional[List[ActivityTypes]] = None,
708+
activities: Optional[Sequence[ActivityTypes]] = None,
704709
status: Optional[Status] = None,
705710
since: int = 0,
706711
afk: bool = False,
@@ -712,17 +717,15 @@ async def change_presence(
712717
else:
713718
activities_data = []
714719

715-
if status == 'idle':
716-
since = int(time.time() * 1000)
717-
718720
payload = {
719721
'op': self.PRESENCE,
720-
'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status or 'online')},
722+
'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status or 'unknown')},
721723
}
722724

723-
sent = utils._to_json(payload)
724-
_log.debug('Sending "%s" to change presence.', sent)
725-
await self.send(sent)
725+
_log.debug('Sending %s to change presence.', payload['d'])
726+
await self.send_as_json(payload)
727+
self.afk = afk
728+
self.idle_since = since
726729

727730
async def request_lazy_guild(
728731
self,

discord/state.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,14 @@ def __init__(
615615
else:
616616
status = str(status)
617617

618+
idle_since = options.get('idle_since', None)
619+
if idle_since:
620+
if not isinstance(idle_since, datetime.datetime):
621+
raise TypeError('idle_since parameter must be a datetime.datetime')
622+
since = int(idle_since.timestamp() * 1000)
623+
else:
624+
since = 0
625+
618626
self._chunk_guilds: bool = options.get('chunk_guilds_at_startup', True)
619627
self._request_guilds = options.get('request_guilds', True)
620628

@@ -628,6 +636,8 @@ def __init__(
628636
self.member_cache_flags: MemberCacheFlags = cache_flags
629637
self._activities: List[ActivityPayload] = activities
630638
self._status: Optional[str] = status
639+
self._afk: bool = options.get('afk', False)
640+
self._idle_since: int = since
631641

632642
if cache_flags._empty:
633643
self.store_user = self.create_user

0 commit comments

Comments
 (0)