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.
activity: Optional[:class:`.BaseActivity`]
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`]
Control how the client handles mentions by default on every message sent.
@ -324,6 +332,7 @@ class Client:
if status or activities:
if status is None:
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))
@property
@ -684,11 +693,21 @@ class Client:
):
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
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:
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)
# Hooks
@ -1230,6 +1249,32 @@ class Client:
activities = (activity,) if activity else activities
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
def allowed_mentions(self) -> Optional[AllowedMentions]:
"""Optional[:class:`~discord.AllowedMentions`]: The allowed mention configuration.
@ -1606,21 +1651,30 @@ class Client:
async def change_presence(
self,
*,
activity: Optional[ActivityTypes] = None,
activities: Optional[List[ActivityTypes]] = None,
status: Optional[Status] = None,
afk: bool = False,
activity: Optional[ActivityTypes] = MISSING,
activities: List[ActivityTypes] = MISSING,
status: Status = MISSING,
afk: bool = MISSING,
idle_since: Optional[datetime] = MISSING,
edit_settings: bool = True,
) -> None:
"""|coro|
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
Edits are no longer in place.
Added option to update settings.
.. versionchanged:: 2.0
This function will now raise :exc:`TypeError` instead of
``InvalidArgument``.
@ -1636,55 +1690,79 @@ class Client:
----------
activity: Optional[:class:`.BaseActivity`]
The activity being done. ``None`` if no activity is done.
activities: Optional[List[:class:`.BaseActivity`]]
A list of the activities being done. ``None`` if no activities
are done. Cannot be sent with ``activity``.
status: Optional[:class:`.Status`]
Indicates what status to change to. If ``None``, then
:attr:`.Status.online` is used.
activities: List[:class:`.BaseActivity`]
A list of the activities being done. Cannot be sent with ``activity``.
.. versionadded:: 2.0
status: :class:`.Status`
Indicates what status to change to.
afk: :class:`bool`
Indicates if you are going AFK. This allows the Discord
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`
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
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.
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
------
TypeError
The ``activity`` parameter is not the proper type.
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')
activities = activities or activity and [activity]
if activities is None:
activities = []
if status is None:
status = Status.online
elif status is Status.offline:
status = Status.invisible
skip_activities = False
if activities is MISSING:
if activity is not MISSING:
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:
custom_activity = None
if idle_since is MISSING:
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:
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
await self.ws.change_presence(status=status, activities=activities, afk=afk, since=since)
if edit_settings and self.settings:
payload: Dict[str, Any] = {}
if status != getattr(self.settings, 'status', None):
if not skip_status and status != self.settings.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
if payload and self.settings:
if payload:
await self.settings.edit(**payload)
async def change_voice_state(

39
discord/gateway.py

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

10
discord/state.py

@ -615,6 +615,14 @@ class ConnectionState:
else:
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._request_guilds = options.get('request_guilds', True)
@ -628,6 +636,8 @@ class ConnectionState:
self.member_cache_flags: MemberCacheFlags = cache_flags
self._activities: List[ActivityPayload] = activities
self._status: Optional[str] = status
self._afk: bool = options.get('afk', False)
self._idle_since: int = since
if cache_flags._empty:
self.store_user = self.create_user

Loading…
Cancel
Save