Added presence update on change of profile information and config flags for selective presence tracking

Signed-off-by: Michael Hollister <michael@futo.org>
This commit is contained in:
Michael Hollister 2024-03-10 22:54:48 -05:00
parent 37558d5e4c
commit 16a21470dd
12 changed files with 150 additions and 10 deletions

View file

@ -0,0 +1 @@
Added presence update on change of profile information and config flags for selective presence tracking. Contributed by @Michael-Hollister.

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.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

View file

@ -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:

View file

@ -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

View file

@ -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

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

@ -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]:

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,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

View file

@ -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:
-- <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,
)
]
)