From 16a21470dd91c3f6490e1d74709e3947c5029511 Mon Sep 17 00:00:00 2001 From: Michael Hollister Date: Sun, 10 Mar 2024 22:54:48 -0500 Subject: [PATCH] Added presence update on change of profile information and config flags for selective presence tracking Signed-off-by: Michael Hollister --- changelog.d/16992.feature | 1 + synapse/api/presence.py | 4 ++ synapse/config/server.py | 10 +++++ synapse/federation/federation_server.py | 11 +++++ synapse/handlers/presence.py | 43 +++++++++++++++++-- synapse/handlers/profile.py | 26 +++++++++++ synapse/replication/tcp/streams/_base.py | 2 + synapse/storage/databases/main/presence.py | 26 +++++++++-- synapse/storage/schema/__init__.py | 9 ++-- .../delta/85/01presence_stream_updates.sql | 20 +++++++++ tests/api/test_filtering.py | 4 ++ tests/handlers/test_presence.py | 4 ++ 12 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 changelog.d/16992.feature create mode 100644 synapse/storage/schema/main/delta/85/01presence_stream_updates.sql diff --git a/changelog.d/16992.feature b/changelog.d/16992.feature new file mode 100644 index 0000000000..903916be7e --- /dev/null +++ b/changelog.d/16992.feature @@ -0,0 +1 @@ +Added presence update on change of profile information and config flags for selective presence tracking. Contributed by @Michael-Hollister. diff --git a/synapse/api/presence.py b/synapse/api/presence.py index 28c10403ce..891746dcd1 100644 --- a/synapse/api/presence.py +++ b/synapse/api/presence.py @@ -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, ) diff --git a/synapse/config/server.py b/synapse/config/server.py index a2b2305776..e510205b98 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -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.sync_presence_tracking = presence_config.get( + "sync_presence_tracking", True + ) + + # Disabling federation presence tracking + self.federation_presence_tracking = presence_config.get( + "federation_presence_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 diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 7ffc650aa1..d8dc1484b0 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -1428,6 +1428,17 @@ class FederationHandlerRegistry: if not self.config.server.track_presence and edu_type == EduTypes.PRESENCE: return + if ( + not self.config.server.federation_presence_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: diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 37ee625f71..6ea4cf7867 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -201,6 +201,7 @@ class BasePresenceHandler(abc.ABC): self._presence_enabled = hs.config.server.presence_enabled self._track_presence = hs.config.server.track_presence + self._sync_presence_tracking = hs.config.server.sync_presence_tracking self._federation = None if hs.should_send_federation(): @@ -451,6 +452,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 +582,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._sync_presence_tracking + ): return _NullContextManager() # Note that this causes last_active_ts to be incremented which is not @@ -648,6 +655,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 +1149,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._sync_presence_tracking + ): return _NullContextManager() curr_sync = self._user_device_to_num_current_syncs.get((user_id, device_id), 0) @@ -1340,6 +1353,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 +1384,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 +1431,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 +1653,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 +1689,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 +1754,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 +1764,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 +1778,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 diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index e51e282a9f..968b3f5fb4 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -200,6 +200,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: @@ -293,6 +306,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 diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 661206c841..8e17257e8e 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -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 diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index 567c2d30bd..f47f787e7c 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -181,6 +181,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore) "last_user_sync_ts", "status_msg", "currently_active", + "displayname", + "avatar_url", "instance_name", ), values=[ @@ -193,6 +195,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) @@ -232,7 +236,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 @@ -285,6 +290,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore) "last_user_sync_ts", "status_msg", "currently_active", + "displayname", + "avatar_url", ), desc="get_presence_for_users", ), @@ -299,8 +306,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( @@ -427,6 +436,8 @@ class PresenceStore(PresenceBackgroundUpdateStore, CacheInvalidationWorkerStore) "last_user_sync_ts", "status_msg", "currently_active", + "displayname", + "avatar_url", ), order_direction="ASC", ), @@ -440,6 +451,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, @@ -449,6 +462,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 @@ -471,7 +486,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 != ?" ) @@ -489,8 +505,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]: diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index 0dc5d24249..203a601f3b 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -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,12 +139,15 @@ 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 """ SCHEMA_COMPAT_VERSION = ( - # Transitive links are no longer written to `event_auth_chain_links` - 84 + # Added displayname and avatar_url columns to presence_stream + 86 ) """Limit on how far the synapse codebase can be rolled back without breaking db compat diff --git a/synapse/storage/schema/main/delta/85/01presence_stream_updates.sql b/synapse/storage/schema/main/delta/85/01presence_stream_updates.sql new file mode 100644 index 0000000000..54a32ba3a8 --- /dev/null +++ b/synapse/storage/schema/main/delta/85/01presence_stream_updates.sql @@ -0,0 +1,20 @@ +-- +-- This file is licensed under the Affero General Public License (AGPL) version 3. +-- +-- Copyright (C) 2023 New Vector, Ltd +-- +-- +-- 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: +-- . + +ALTER TABLE presence_stream ADD COLUMN displayname TEXT; +ALTER TABLE presence_stream ADD COLUMN avatar_url TEXT; diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 743c52d969..0051653026 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -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, ), ] diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index cc630d606c..ae99bda15e 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -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, ) ] )