Browse Source

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
pull/10109/head
dolfies 1 year ago
committed by GitHub
parent
commit
0847b02ae1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 134
      discord/client.py
  2. 39
      discord/gateway.py
  3. 10
      discord/state.py

134
discord/client.py

@ -186,6 +186,14 @@ class Client:
A status to start your presence with upon logging on to Discord. A status to start your presence with upon logging on to Discord.
activity: Optional[:class:`.BaseActivity`] activity: Optional[:class:`.BaseActivity`]
An activity to start your presence with upon logging on to Discord. An activity to start your presence with upon logging on to Discord.
activities: List[:class:`.BaseActivity`]
A list of activities to start your presence with upon logging on to Discord. Cannot be sent with ``activity``.
.. versionadded:: 2.0
afk: :class:`bool`
Whether to start your session as AFK. Defaults to ``False``.
.. versionadded:: 2.1
allowed_mentions: Optional[:class:`AllowedMentions`] allowed_mentions: Optional[:class:`AllowedMentions`]
Control how the client handles mentions by default on every message sent. Control how the client handles mentions by default on every message sent.
@ -324,6 +332,7 @@ class Client:
if status or activities: if status or activities:
if status is None: if status is None:
status = getattr(state.settings, 'status', None) or Status.unknown status = getattr(state.settings, 'status', None) or Status.unknown
_log.debug('Setting initial presence to %s %s', status, activities)
self.loop.create_task(self.change_presence(activities=activities, status=status)) self.loop.create_task(self.change_presence(activities=activities, status=status))
@property @property
@ -684,11 +693,21 @@ class Client:
): ):
return # Nothing changed return # Nothing changed
current_activity = None
for activity in self.activities:
if activity.type != ActivityType.custom:
current_activity = activity
break
if new_settings.status == self.client_status and new_settings.custom_activity == current_activity:
return # Nothing changed
status = new_settings.status status = new_settings.status
activities = [a for a in self.activities if a.type != ActivityType.custom] activities = [a for a in self.client_activities if a.type != ActivityType.custom]
if new_settings.custom_activity is not None: if new_settings.custom_activity is not None:
activities.append(new_settings.custom_activity) activities.append(new_settings.custom_activity)
_log.debug('Syncing presence to %s %s', status, new_settings.custom_activity)
await self.change_presence(status=status, activities=activities, edit_settings=False) await self.change_presence(status=status, activities=activities, edit_settings=False)
# Hooks # Hooks
@ -1230,6 +1249,32 @@ class Client:
activities = (activity,) if activity else activities activities = (activity,) if activity else activities
return activities or tuple() return activities or tuple()
def is_afk(self) -> bool:
""":class:`bool`: Indicates if the client is currently AFK.
This allows the Discord client to know how to handle push notifications
better for you in case you are away from your keyboard.
.. versionadded:: 2.1
"""
if self.ws:
return self.ws.afk
return False
@property
def idle_since(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: When the client went idle.
This indicates that you are truly idle and not just lying.
.. versionadded:: 2.1
"""
ws = self.ws
if ws is None or not ws.idle_since:
return None
return utils.parse_timestamp(ws.idle_since)
@property @property
def allowed_mentions(self) -> Optional[AllowedMentions]: def allowed_mentions(self) -> Optional[AllowedMentions]:
"""Optional[:class:`~discord.AllowedMentions`]: The allowed mention configuration. """Optional[:class:`~discord.AllowedMentions`]: The allowed mention configuration.
@ -1606,21 +1651,30 @@ class Client:
async def change_presence( async def change_presence(
self, self,
*, *,
activity: Optional[ActivityTypes] = None, activity: Optional[ActivityTypes] = MISSING,
activities: Optional[List[ActivityTypes]] = None, activities: List[ActivityTypes] = MISSING,
status: Optional[Status] = None, status: Status = MISSING,
afk: bool = False, afk: bool = MISSING,
idle_since: Optional[datetime] = MISSING,
edit_settings: bool = True, edit_settings: bool = True,
) -> None: ) -> None:
"""|coro| """|coro|
Changes the client's presence. Changes the client's presence.
.. versionchanged:: 2.1
The default value for parameters is now the current value.
``None`` is no longer a valid value for most; you must explicitly
set it to the default value if you want to reset it.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
Edits are no longer in place. Edits are no longer in place.
Added option to update settings. Added option to update settings.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
This function will now raise :exc:`TypeError` instead of This function will now raise :exc:`TypeError` instead of
``InvalidArgument``. ``InvalidArgument``.
@ -1636,55 +1690,79 @@ class Client:
---------- ----------
activity: Optional[:class:`.BaseActivity`] activity: Optional[:class:`.BaseActivity`]
The activity being done. ``None`` if no activity is done. The activity being done. ``None`` if no activity is done.
activities: Optional[List[:class:`.BaseActivity`]] activities: List[:class:`.BaseActivity`]
A list of the activities being done. ``None`` if no activities A list of the activities being done. Cannot be sent with ``activity``.
are done. Cannot be sent with ``activity``.
status: Optional[:class:`.Status`] .. versionadded:: 2.0
Indicates what status to change to. If ``None``, then status: :class:`.Status`
:attr:`.Status.online` is used. Indicates what status to change to.
afk: :class:`bool` afk: :class:`bool`
Indicates if you are going AFK. This allows the Discord Indicates if you are going AFK. This allows the Discord
client to know how to handle push notifications better client to know how to handle push notifications better
for you in case you are actually idle and not lying. for you in case you are away from your keyboard.
idle_since: Optional[:class:`datetime.datetime`]
When the client went idle. This indicates that you are
truly idle and not just lying.
edit_settings: :class:`bool` edit_settings: :class:`bool`
Whether to update the settings with the new status and/or Whether to update user settings with the new status and/or
custom activity. This will broadcast the change and cause custom activity. This will broadcast the change and cause
all connected (official) clients to change presence as well. all connected (official) clients to change presence as well.
This should be set to ``False`` for idle changes.
Required for setting/editing ``expires_at`` for custom activities. Required for setting/editing ``expires_at`` for custom activities.
It's not recommended to change this, as setting it to ``False`` causes undefined behavior. It's not recommended to change this, as setting it to ``False``
can cause undefined behavior.
Raises Raises
------ ------
TypeError TypeError
The ``activity`` parameter is not the proper type. The ``activity`` parameter is not the proper type.
Both ``activity`` and ``activities`` were passed. Both ``activity`` and ``activities`` were passed.
ValueError
More than one custom activity was passed.
""" """
if activity and activities: if activity is not MISSING and activities is not MISSING:
raise TypeError('Cannot pass both activity and activities') raise TypeError('Cannot pass both activity and activities')
activities = activities or activity and [activity]
if activities is None:
activities = []
if status is None: skip_activities = False
status = Status.online if activities is MISSING:
elif status is Status.offline: if activity is not MISSING:
status = Status.invisible activities = [activity] if activity else []
else:
activities = list(self.client_activities)
skip_activities = True
else:
activities = activities or []
await self.ws.change_presence(status=status, activities=activities, afk=afk) skip_status = status is MISSING
if status is MISSING:
status = self.client_status
if status is Status.offline:
status = Status.invisible
if edit_settings: if idle_since is MISSING:
custom_activity = None since = self.ws.idle_since if self.ws else 0
else:
since = int(idle_since.timestamp() * 1000) if idle_since else 0
custom_activity = None
if not skip_activities:
for activity in activities: for activity in activities:
if getattr(activity, 'type', None) is ActivityType.custom: if getattr(activity, 'type', None) is ActivityType.custom:
if custom_activity is not None:
raise ValueError('More than one custom activity was passed')
custom_activity = activity custom_activity = activity
await self.ws.change_presence(status=status, activities=activities, afk=afk, since=since)
if edit_settings and self.settings:
payload: Dict[str, Any] = {} payload: Dict[str, Any] = {}
if status != getattr(self.settings, 'status', None): if not skip_status and status != self.settings.status:
payload['status'] = status payload['status'] = status
if custom_activity != getattr(self.settings, 'custom_activity', None): if not skip_activities and custom_activity != self.settings.custom_activity:
payload['custom_activity'] = custom_activity payload['custom_activity'] = custom_activity
if payload and self.settings: if payload:
await self.settings.edit(**payload) await self.settings.edit(**payload)
async def change_voice_state( async def change_voice_state(

39
discord/gateway.py

@ -32,7 +32,7 @@ import threading
import traceback import traceback
import zlib import zlib
from typing import Any, Callable, Coroutine, Dict, List, TYPE_CHECKING, NamedTuple, Optional, TypeVar from typing import Any, Callable, Coroutine, Dict, List, TYPE_CHECKING, NamedTuple, Optional, Sequence, TypeVar
import aiohttp import aiohttp
import yarl import yarl
@ -334,6 +334,10 @@ class DiscordWebSocket:
self._close_code: Optional[int] = None self._close_code: Optional[int] = None
self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter() self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter()
# Presence state tracking
self.afk: bool = False
self.idle_since: int = 0
@property @property
def open(self) -> bool: def open(self) -> bool:
return not self.socket.closed return not self.socket.closed
@ -395,6 +399,8 @@ class DiscordWebSocket:
ws._user_agent = client.http.user_agent ws._user_agent = client.http.user_agent
ws._super_properties = client.http.super_properties ws._super_properties = client.http.super_properties
ws._zlib_enabled = zlib ws._zlib_enabled = zlib
ws.afk = client._connection._afk
ws.idle_since = client._connection._idle_since
if client._enable_debug_events: if client._enable_debug_events:
ws.send = ws.debug_send ws.send = ws.debug_send
@ -456,15 +462,13 @@ class DiscordWebSocket:
# but that needs more testing... # but that needs more testing...
presence = { presence = {
'status': 'unknown', 'status': 'unknown',
'since': 0, 'since': self.idle_since,
'activities': [], 'activities': [],
'afk': False, 'afk': self.afk,
} }
existing = self._connection.current_session existing = self._connection.current_session
if existing is not None: if existing is not None:
presence['status'] = str(existing.status) if existing.status is not Status.offline else 'invisible' presence['status'] = str(existing.status) if existing.status is not Status.offline else 'invisible'
if existing.status == Status.idle:
presence['since'] = int(time.time() * 1000)
presence['activities'] = [a.to_dict() for a in existing.activities] presence['activities'] = [a.to_dict() for a in existing.activities]
# else: # else:
# presence['status'] = self._connection._status or 'unknown' # presence['status'] = self._connection._status or 'unknown'
@ -482,11 +486,12 @@ class DiscordWebSocket:
'client_state': { 'client_state': {
'api_code_version': 0, 'api_code_version': 0,
'guild_versions': {}, 'guild_versions': {},
'highest_last_message_id': '0', # 'highest_last_message_id': '0',
'private_channels_version': '0', # 'initial_guild_id': None,
'read_state_version': 0, # 'private_channels_version': '0',
'user_guild_settings_version': -1, # 'read_state_version': 0,
'user_settings_version': -1, # 'user_guild_settings_version': -1,
# 'user_settings_version': -1,
}, },
}, },
} }
@ -700,7 +705,7 @@ class DiscordWebSocket:
async def change_presence( async def change_presence(
self, self,
*, *,
activities: Optional[List[ActivityTypes]] = None, activities: Optional[Sequence[ActivityTypes]] = None,
status: Optional[Status] = None, status: Optional[Status] = None,
since: int = 0, since: int = 0,
afk: bool = False, afk: bool = False,
@ -712,17 +717,15 @@ class DiscordWebSocket:
else: else:
activities_data = [] activities_data = []
if status == 'idle':
since = int(time.time() * 1000)
payload = { payload = {
'op': self.PRESENCE, 'op': self.PRESENCE,
'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status or 'online')}, 'd': {'activities': activities_data, 'afk': afk, 'since': since, 'status': str(status or 'unknown')},
} }
sent = utils._to_json(payload) _log.debug('Sending %s to change presence.', payload['d'])
_log.debug('Sending "%s" to change presence.', sent) await self.send_as_json(payload)
await self.send(sent) self.afk = afk
self.idle_since = since
async def request_lazy_guild( async def request_lazy_guild(
self, self,

10
discord/state.py

@ -615,6 +615,14 @@ class ConnectionState:
else: else:
status = str(status) status = str(status)
idle_since = options.get('idle_since', None)
if idle_since:
if not isinstance(idle_since, datetime.datetime):
raise TypeError('idle_since parameter must be a datetime.datetime')
since = int(idle_since.timestamp() * 1000)
else:
since = 0
self._chunk_guilds: bool = options.get('chunk_guilds_at_startup', True) self._chunk_guilds: bool = options.get('chunk_guilds_at_startup', True)
self._request_guilds = options.get('request_guilds', True) self._request_guilds = options.get('request_guilds', True)
@ -628,6 +636,8 @@ class ConnectionState:
self.member_cache_flags: MemberCacheFlags = cache_flags self.member_cache_flags: MemberCacheFlags = cache_flags
self._activities: List[ActivityPayload] = activities self._activities: List[ActivityPayload] = activities
self._status: Optional[str] = status self._status: Optional[str] = status
self._afk: bool = options.get('afk', False)
self._idle_since: int = since
if cache_flags._empty: if cache_flags._empty:
self.store_user = self.create_user self.store_user = self.create_user

Loading…
Cancel
Save