mirror of
https://github.com/element-hq/synapse
synced 2024-06-30 16:03:32 +00:00
Merge 9716d53627
into 2c36a679ae
This commit is contained in:
commit
3b3baafd1d
1
changelog.d/16992.feature
Normal file
1
changelog.d/16992.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Added presence tracking of user profile updates and config flags for disabling user activity tracking. Contributed by @Michael-Hollister.
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue