This commit is contained in:
Michael Hollister 2024-06-14 10:21:43 +01:00 committed by GitHub
commit 3b3baafd1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 198 additions and 10 deletions

View file

@ -0,0 +1 @@
Added presence tracking of user profile updates and config flags for disabling user activity tracking. Contributed by @Michael-Hollister.

View file

@ -246,6 +246,8 @@ Example configuration:
```yaml
presence:
enabled: false
local_activity_tracking: true
remote_activity_tracking: true
```
`enabled` can also be set to a special value of "untracked" which ignores updates
@ -254,6 +256,21 @@ received via clients and federation, while still accepting updates from the
*The "untracked" option was added in Synapse 1.96.0.*
Enabling presence tracking can be resource intensive for the presence handler when server-side
tracking of user activity is enabled. Below are some additional configuration options which may
help improve the performance of the presence feature without outright disabling it:
* `local_activity_tracking` (Default enabled): Determines if the server tracks a user's activity
when syncing or fetching events. If disabled, the server will not automatically update the
user's presence activity when the /sync or /events endpoints are called. Note that client
applications can still update their presence by calling the presence /status endpoint.
* `remote_activity_tracking` (Default enabled): Determines if the server will accept presence
EDUs from remote servers that are exclusively user activity updates. If disabled, the server
will reject processing these EDUs. However if a presence EDU contains profile updates to any of
the `status_msg`, `displayname`, or `avatar_url` fields, then the server will accept the EDU.
If the presence `enabled` field is set to "untracked", then these options will both act as if
set to false.
---
### `require_auth_for_profile_requests`

View file

@ -83,6 +83,8 @@ class UserPresenceState:
last_user_sync_ts: int
status_msg: Optional[str]
currently_active: bool
displayname: Optional[str]
avatar_url: Optional[str]
def as_dict(self) -> JsonDict:
return attr.asdict(self)
@ -101,4 +103,6 @@ class UserPresenceState:
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
)

View file

@ -384,6 +384,16 @@ class ServerConfig(Config):
# Whether to internally track presence, requires that presence is enabled,
self.track_presence = self.presence_enabled and presence_enabled != "untracked"
# Disabling server-side presence tracking
self.presence_local_activity_tracking = presence_config.get(
"local_activity_tracking", True
)
# Disabling federation presence tracking
self.presence_remote_activity_tracking = presence_config.get(
"remote_activity_tracking", True
)
# Custom presence router module
# This is the legacy way of configuring it (the config should now be put in the modules section)
self.presence_router_module_class = None

View file

@ -1425,9 +1425,30 @@ class FederationHandlerRegistry:
self._edu_type_to_instance[edu_type] = instance_names
async def on_edu(self, edu_type: str, origin: str, content: dict) -> None:
"""Passes an EDU to a registered handler if one exists
This potentially modifies the `content` dict for `m.presence` EDUs when
presence `remote_activity_tracking` is disabled.
Args:
edu_type: The type of the incoming EDU to process
origin: The server we received the event from
content: The content of the EDU
"""
if not self.config.server.track_presence and edu_type == EduTypes.PRESENCE:
return
if (
not self.config.server.presence_remote_activity_tracking
and edu_type == EduTypes.PRESENCE
):
filtered_edus = []
for e in content["push"]:
# Process only profile presence updates to reduce resource impact
if "status_msg" in e or "displayname" in e or "avatar_url" in e:
filtered_edus.append(e)
content["push"] = filtered_edus
# Check if we have a handler on this instance
handler = self.edu_handlers.get(edu_type)
if handler:

View file

@ -201,6 +201,9 @@ class BasePresenceHandler(abc.ABC):
self._presence_enabled = hs.config.server.presence_enabled
self._track_presence = hs.config.server.track_presence
self._presence_local_activity_tracking = (
hs.config.server.presence_local_activity_tracking
)
self._federation = None
if hs.should_send_federation():
@ -451,6 +454,8 @@ class BasePresenceHandler(abc.ABC):
state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": current_presence_state.displayname,
"avatar_url": current_presence_state.avatar_url,
}
# Copy the presence state to the tip of the presence stream.
@ -579,7 +584,11 @@ class WorkerPresenceHandler(BasePresenceHandler):
Called by the sync and events servlets to record that a user has connected to
this worker and is waiting for some events.
"""
if not affect_presence or not self._track_presence:
if (
not affect_presence
or not self._track_presence
or not self._presence_local_activity_tracking
):
return _NullContextManager()
# Note that this causes last_active_ts to be incremented which is not
@ -648,6 +657,8 @@ class WorkerPresenceHandler(BasePresenceHandler):
row.last_user_sync_ts,
row.status_msg,
row.currently_active,
row.displayname,
row.avatar_url,
)
for row in rows
]
@ -1140,7 +1151,11 @@ class PresenceHandler(BasePresenceHandler):
client that is being used by a user.
presence_state: The presence state indicated in the sync request
"""
if not affect_presence or not self._track_presence:
if (
not affect_presence
or not self._track_presence
or not self._presence_local_activity_tracking
):
return _NullContextManager()
curr_sync = self._user_device_to_num_current_syncs.get((user_id, device_id), 0)
@ -1340,6 +1355,8 @@ class PresenceHandler(BasePresenceHandler):
new_fields["status_msg"] = push.get("status_msg", None)
new_fields["currently_active"] = push.get("currently_active", False)
new_fields["displayname"] = push.get("displayname", None)
new_fields["avatar_url"] = push.get("avatar_url", None)
prev_state = await self.current_state_for_user(user_id)
updates.append(prev_state.copy_and_replace(**new_fields))
@ -1369,6 +1386,8 @@ class PresenceHandler(BasePresenceHandler):
the `state` dict.
"""
status_msg = state.get("status_msg", None)
displayname = state.get("displayname", None)
avatar_url = state.get("avatar_url", None)
presence = state["presence"]
if presence not in self.VALID_PRESENCE:
@ -1414,6 +1433,8 @@ class PresenceHandler(BasePresenceHandler):
else:
# Syncs do not override the status message.
new_fields["status_msg"] = status_msg
new_fields["displayname"] = displayname
new_fields["avatar_url"] = avatar_url
await self._update_states(
[prev_state.copy_and_replace(**new_fields)], force_notify=force_notify
@ -1634,6 +1655,8 @@ class PresenceHandler(BasePresenceHandler):
if state.state != PresenceState.OFFLINE
or now - state.last_active_ts < 7 * 24 * 60 * 60 * 1000
or state.status_msg is not None
or state.displayname is not None
or state.avatar_url is not None
]
await self._federation_queue.send_presence_to_destinations(
@ -1668,6 +1691,14 @@ def should_notify(
notify_reason_counter.labels(user_location, "status_msg_change").inc()
return True
if old_state.displayname != new_state.displayname:
notify_reason_counter.labels(user_location, "displayname_change").inc()
return True
if old_state.avatar_url != new_state.avatar_url:
notify_reason_counter.labels(user_location, "avatar_url_change").inc()
return True
if old_state.state != new_state.state:
notify_reason_counter.labels(user_location, "state_change").inc()
state_transition_counter.labels(
@ -1725,6 +1756,8 @@ def format_user_presence_state(
* status_msg: Optional. Included if `status_msg` is set on `state`. The user's
status.
* currently_active: Optional. Included only if `state.state` is "online".
* displayname: Optional. The current display name for this user, if any.
* avatar_url: Optional. The current avatar URL for this user, if any.
Example:
@ -1733,7 +1766,9 @@ def format_user_presence_state(
"user_id": "@alice:example.com",
"last_active_ago": 16783813918,
"status_msg": "Hello world!",
"currently_active": True
"currently_active": True,
"displayname": "Alice",
"avatar_url": "mxc://localhost/wefuiwegh8742w"
}
"""
content: JsonDict = {"presence": state.state}
@ -1745,6 +1780,10 @@ def format_user_presence_state(
content["status_msg"] = state.status_msg
if state.state == PresenceState.ONLINE:
content["currently_active"] = state.currently_active
if state.displayname:
content["displayname"] = state.displayname
if state.avatar_url:
content["avatar_url"] = state.avatar_url
return content

View file

@ -202,6 +202,19 @@ class ProfileHandler:
if propagate:
await self._update_join_states(requester, target_user)
if self.hs.config.server.track_presence:
presence_handler = self.hs.get_presence_handler()
current_presence_state = await presence_handler.get_state(target_user)
state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": new_displayname,
"avatar_url": current_presence_state.avatar_url,
}
await presence_handler.set_state(target_user, requester.device_id, state)
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
if self.hs.is_mine(target_user):
try:
@ -295,6 +308,19 @@ class ProfileHandler:
if propagate:
await self._update_join_states(requester, target_user)
if self.hs.config.server.track_presence:
presence_handler = self.hs.get_presence_handler()
current_presence_state = await presence_handler.get_state(target_user)
state = {
"presence": current_presence_state.state,
"status_message": current_presence_state.status_msg,
"displayname": current_presence_state.displayname,
"avatar_url": new_avatar_url,
}
await presence_handler.set_state(target_user, requester.device_id, state)
@cached()
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool:
"""Check that the size and content type of the avatar at the given MXC URI are

View file

@ -330,6 +330,8 @@ class PresenceStream(_StreamFromIdGen):
last_user_sync_ts: int
status_msg: str
currently_active: bool
displayname: str
avatar_url: str
NAME = "presence"
ROW_TYPE = PresenceStreamRow

View file

@ -174,6 +174,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
"instance_name",
),
values=[
@ -186,6 +188,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
state.last_user_sync_ts,
state.status_msg,
state.currently_active,
state.displayname,
state.avatar_url,
self._instance_name,
)
for stream_id, state in zip(stream_orderings, presence_states)
@ -225,7 +229,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
sql = """
SELECT stream_id, user_id, state, last_active_ts,
last_federation_update_ts, last_user_sync_ts,
status_msg, currently_active
status_msg, currently_active, displayname,
avatar_url
FROM presence_stream
WHERE ? < stream_id AND stream_id <= ?
ORDER BY stream_id ASC
@ -264,7 +269,19 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
# TODO All these columns are nullable, but we don't expect that:
# https://github.com/matrix-org/synapse/issues/16467
rows = cast(
List[Tuple[str, str, int, int, int, Optional[str], Union[int, bool]]],
List[
Tuple[
str,
str,
int,
int,
int,
Optional[str],
Union[int, bool],
Optional[str],
Optional[str],
]
],
await self.db_pool.simple_select_many_batch(
table="presence_stream",
column="user_id",
@ -278,6 +295,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
),
desc="get_presence_for_users",
),
@ -292,8 +311,10 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
}
async def should_user_receive_full_presence_with_token(
@ -403,7 +424,19 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
# TODO All these columns are nullable, but we don't expect that:
# https://github.com/matrix-org/synapse/issues/16467
rows = cast(
List[Tuple[str, str, int, int, int, Optional[str], Union[int, bool]]],
List[
Tuple[
str,
str,
int,
int,
int,
Optional[str],
Union[int, bool],
Optional[str],
Optional[str],
]
],
await self.db_pool.runInteraction(
"get_presence_for_all_users",
self.db_pool.simple_select_list_paginate_txn,
@ -420,6 +453,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
"last_user_sync_ts",
"status_msg",
"currently_active",
"displayname",
"avatar_url",
),
order_direction="ASC",
),
@ -433,6 +468,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
last_user_sync_ts,
status_msg,
currently_active,
displayname,
avatar_url,
) in rows:
users_to_state[user_id] = UserPresenceState(
user_id=user_id,
@ -442,6 +479,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)
# We've run out of updates to query
@ -464,7 +503,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
# query.
sql = (
"SELECT user_id, state, last_active_ts, last_federation_update_ts,"
" last_user_sync_ts, status_msg, currently_active FROM presence_stream"
" last_user_sync_ts, status_msg, currently_active, displayname, avatar_url "
" FROM presence_stream"
" WHERE state != ?"
)
@ -482,8 +522,10 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore)
last_user_sync_ts=last_user_sync_ts,
status_msg=status_msg,
currently_active=bool(currently_active),
displayname=displayname,
avatar_url=avatar_url,
)
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active in rows
for user_id, state, last_active_ts, last_federation_update_ts, last_user_sync_ts, status_msg, currently_active, displayname, avatar_url, in rows
]
def take_presence_startup_info(self) -> List[UserPresenceState]:

View file

@ -19,7 +19,7 @@
#
#
SCHEMA_VERSION = 85 # remember to update the list below when updating
SCHEMA_VERSION = 86 # remember to update the list below when updating
"""Represents the expectations made by the codebase about the database schema
This should be incremented whenever the codebase changes its requirements on the
@ -139,6 +139,9 @@ Changes in SCHEMA_VERSION = 84
Changes in SCHEMA_VERSION = 85
- Add a column `suspended` to the `users` table
Changes in SCHEMA_VERSION = 86
- Added displayname and avatar_url columns to presence_stream
"""

View file

@ -0,0 +1,15 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2024 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
ALTER TABLE presence_stream ADD COLUMN displayname TEXT;
ALTER TABLE presence_stream ADD COLUMN avatar_url TEXT;

View file

@ -450,6 +450,8 @@ class FilteringTestCase(unittest.HomeserverTestCase):
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
),
]
@ -478,6 +480,8 @@ class FilteringTestCase(unittest.HomeserverTestCase):
last_user_sync_ts=0,
status_msg=None,
currently_active=False,
displayname=None,
avatar_url=None,
),
]

View file

@ -366,6 +366,8 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
last_user_sync_ts=1,
status_msg="I'm online!",
currently_active=True,
displayname=None,
avatar_url=None,
)
presence_states.append(presence_state)
@ -718,6 +720,8 @@ class PresenceHandlerInitTestCase(unittest.HomeserverTestCase):
last_user_sync_ts=now,
status_msg=None,
currently_active=True,
displayname=None,
avatar_url=None,
)
]
)