From e766f325affcd32b2fd08232439e7a47d6186fe8 Mon Sep 17 00:00:00 2001 From: PizZaKatZe <78648379+pizkaz@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:59:11 +0200 Subject: [PATCH] fix: Compute user last seen timestamp from last seen devices (#18948) ## Fix last seen timestamp in `/_synapse/admin/v2/users` response Fixes #18955 The last seen timestamps contained in `/_synapse/admin/v2/users` responses were computed as follows: ```sql [...] LEFT JOIN ( SELECT user_id, MAX(last_seen) AS last_seen_ts FROM user_ips GROUP BY user_id ) ls ON u.name = ls.user_id [...] ``` https://github.com/element-hq/synapse/blob/4367fb2d078c52959aeca0fe6874539c53e8360d/synapse/storage/databases/main/__init__.py#L302C1-L305C44 This leads to empty timestamps (as in: user was never seen) if users are inactive for longer than [`user_ips_max_age`](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#user_ips_max_age). The fix is quite trivial: Use the `devices` table, as this one also contains last seen timestamps but is *not* periodically purged. We are using this for automatic user account deletion (via [synadm](https://codeberg.org/synadm/synadm)) and the patched code works as intended, whereas the unpatched version wants to delete users during long vacations. :face_with_peeking_eye: --- changelog.d/18948.bugfix | 1 + synapse/storage/databases/main/__init__.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelog.d/18948.bugfix diff --git a/changelog.d/18948.bugfix b/changelog.d/18948.bugfix new file mode 100644 index 0000000000..7a8af0a286 --- /dev/null +++ b/changelog.d/18948.bugfix @@ -0,0 +1 @@ +Compute a user's last seen timestamp from their devices' last seen timestamps instead of IPs, because the latter are automatically cleared according to `user_ips_max_age`. diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index de55c452ae..83b480adaf 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -299,10 +299,14 @@ class DataStore( FROM users as u LEFT JOIN profiles AS p ON u.name = p.full_user_id LEFT JOIN erased_users AS eu ON u.name = eu.user_id + LEFT JOIN ( + SELECT user_id, MAX(last_seen) AS last_seen_ts + FROM devices GROUP BY user_id + ) lsd ON u.name = lsd.user_id LEFT JOIN ( SELECT user_id, MAX(last_seen) AS last_seen_ts FROM user_ips GROUP BY user_id - ) ls ON u.name = ls.user_id + ) lsi ON u.name = lsi.user_id {where_clause} """ sql = "SELECT COUNT(*) as total_users " + sql_base @@ -312,7 +316,8 @@ class DataStore( sql = f""" SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, displayname, avatar_url, creation_ts * 1000 as creation_ts, approved, - eu.user_id is not null as erased, last_seen_ts, locked + eu.user_id is not null as erased, + COALESCE(lsd.last_seen_ts, lsi.last_seen_ts) as last_seen_ts, locked {sql_base} ORDER BY {order_by_column} {order}, u.name ASC LIMIT ? OFFSET ?