Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit dcb2778

Browse files
author
Mathieu Velten
authored
Add last_seen_ts to the admin users API (#16218)
1 parent 7213466 commit dcb2778

File tree

10 files changed

+80
-2
lines changed

10 files changed

+80
-2
lines changed

changelog.d/16218.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `last_seen_ts` to the admin users API.

docs/admin_api/user_admin_api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ The following parameters should be set in the URL:
242242
- `displayname` - Users are ordered alphabetically by `displayname`.
243243
- `avatar_url` - Users are ordered alphabetically by avatar URL.
244244
- `creation_ts` - Users are ordered by when the users was created in ms.
245+
- `last_seen_ts` - Users are ordered by when the user was lastly seen in ms.
245246

246247
- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards.
247248
Setting this value to `b` will reverse the above sort order. Defaults to `f`.
@@ -272,6 +273,7 @@ The following fields are returned in the JSON response body:
272273
- `displayname` - string - The user's display name if they have set one.
273274
- `avatar_url` - string - The user's avatar URL if they have set one.
274275
- `creation_ts` - integer - The user's creation timestamp in ms.
276+
- `last_seen_ts` - integer - The user's last activity timestamp in ms.
275277

276278
- `next_token`: string representing a positive integer - Indication for pagination. See above.
277279
- `total` - integer - Total number of media.

synapse/handlers/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]:
7676
"consent_ts",
7777
"user_type",
7878
"is_guest",
79+
"last_seen_ts",
7980
}
8081

8182
if self._msc3866_enabled:

synapse/rest/admin/users.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
132132
UserSortOrder.AVATAR_URL.value,
133133
UserSortOrder.SHADOW_BANNED.value,
134134
UserSortOrder.CREATION_TS.value,
135+
UserSortOrder.LAST_SEEN_TS.value,
135136
),
136137
)
137138

synapse/storage/databases/main/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ def get_users_paginate_txn(
277277
FROM users as u
278278
LEFT JOIN profiles AS p ON u.name = p.full_user_id
279279
LEFT JOIN erased_users AS eu ON u.name = eu.user_id
280+
LEFT JOIN (
281+
SELECT user_id, MAX(last_seen) AS last_seen_ts
282+
FROM user_ips GROUP BY user_id
283+
) ls ON u.name = ls.user_id
280284
{where_clause}
281285
"""
282286
sql = "SELECT COUNT(*) as total_users " + sql_base
@@ -286,7 +290,7 @@ def get_users_paginate_txn(
286290
sql = f"""
287291
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
288292
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved,
289-
eu.user_id is not null as erased
293+
eu.user_id is not null as erased, last_seen_ts
290294
{sql_base}
291295
ORDER BY {order_by_column} {order}, u.name ASC
292296
LIMIT ? OFFSET ?

synapse/storage/databases/main/registration.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,12 @@ def get_user_by_id_txn(txn: LoggingTransaction) -> Optional[Dict[str, Any]]:
206206
consent_server_notice_sent, appservice_id, creation_ts, user_type,
207207
deactivated, COALESCE(shadow_banned, FALSE) AS shadow_banned,
208208
COALESCE(approved, TRUE) AS approved,
209-
COALESCE(locked, FALSE) AS locked
209+
COALESCE(locked, FALSE) AS locked, last_seen_ts
210210
FROM users
211+
LEFT JOIN (
212+
SELECT user_id, MAX(last_seen) AS last_seen_ts
213+
FROM user_ips GROUP BY user_id
214+
) ls ON users.name = ls.user_id
211215
WHERE name = ?
212216
""",
213217
(user_id,),
@@ -268,6 +272,7 @@ async def get_userinfo_by_id(self, user_id: str) -> Optional[UserInfo]:
268272
is_shadow_banned=bool(user_data["shadow_banned"]),
269273
user_id=UserID.from_string(user_data["name"]),
270274
user_type=user_data["user_type"],
275+
last_seen_ts=user_data["last_seen_ts"],
271276
)
272277

273278
async def is_trial_user(self, user_id: str) -> bool:

synapse/storage/databases/main/stats.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class UserSortOrder(Enum):
107107
AVATAR_URL = "avatar_url"
108108
SHADOW_BANNED = "shadow_banned"
109109
CREATION_TS = "creation_ts"
110+
LAST_SEEN_TS = "last_seen_ts"
110111

111112

112113
class StatsStore(StateDeltasStore):

synapse/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ class UserInfo:
946946
is_guest: True if the user is a guest user.
947947
is_shadow_banned: True if the user has been shadow-banned.
948948
user_type: User type (None for normal user, 'support' and 'bot' other options).
949+
last_seen_ts: Last activity timestamp of the user.
949950
"""
950951

951952
user_id: UserID
@@ -958,6 +959,7 @@ class UserInfo:
958959
is_deactivated: bool
959960
is_guest: bool
960961
is_shadow_banned: bool
962+
last_seen_ts: Optional[int]
961963

962964

963965
class UserProfile(TypedDict):

tests/rest/admin/test_user.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
user_directory,
4141
)
4242
from synapse.server import HomeServer
43+
from synapse.storage.databases.main.client_ips import LAST_SEEN_GRANULARITY
4344
from synapse.types import JsonDict, UserID, create_requester
4445
from synapse.util import Clock
4546

@@ -456,6 +457,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
456457
servlets = [
457458
synapse.rest.admin.register_servlets,
458459
login.register_servlets,
460+
room.register_servlets,
459461
]
460462
url = "/_synapse/admin/v2/users"
461463

@@ -506,6 +508,62 @@ def test_all_users(self) -> None:
506508
# Check that all fields are available
507509
self._check_fields(channel.json_body["users"])
508510

511+
def test_last_seen(self) -> None:
512+
"""
513+
Test that last_seen_ts field is properly working.
514+
"""
515+
user1 = self.register_user("u1", "pass")
516+
user1_token = self.login("u1", "pass")
517+
user2 = self.register_user("u2", "pass")
518+
user2_token = self.login("u2", "pass")
519+
user3 = self.register_user("u3", "pass")
520+
user3_token = self.login("u3", "pass")
521+
522+
self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
523+
self.reactor.advance(10)
524+
self.helper.create_room_as(user2, tok=user2_token)
525+
self.reactor.advance(10)
526+
self.helper.create_room_as(user1, tok=user1_token)
527+
self.reactor.advance(10)
528+
self.helper.create_room_as(user3, tok=user3_token)
529+
self.reactor.advance(10)
530+
531+
channel = self.make_request(
532+
"GET",
533+
self.url,
534+
access_token=self.admin_user_tok,
535+
)
536+
537+
self.assertEqual(200, channel.code, msg=channel.json_body)
538+
self.assertEqual(4, len(channel.json_body["users"]))
539+
self.assertEqual(4, channel.json_body["total"])
540+
541+
admin_last_seen = channel.json_body["users"][0]["last_seen_ts"]
542+
user1_last_seen = channel.json_body["users"][1]["last_seen_ts"]
543+
user2_last_seen = channel.json_body["users"][2]["last_seen_ts"]
544+
user3_last_seen = channel.json_body["users"][3]["last_seen_ts"]
545+
self.assertTrue(admin_last_seen > 0 and admin_last_seen < 10000)
546+
self.assertTrue(user2_last_seen > 10000 and user2_last_seen < 20000)
547+
self.assertTrue(user1_last_seen > 20000 and user1_last_seen < 30000)
548+
self.assertTrue(user3_last_seen > 30000 and user3_last_seen < 40000)
549+
550+
self._order_test([self.admin_user, user2, user1, user3], "last_seen_ts")
551+
552+
self.reactor.advance(LAST_SEEN_GRANULARITY / 1000)
553+
self.helper.create_room_as(user1, tok=user1_token)
554+
self.reactor.advance(10)
555+
556+
channel = self.make_request(
557+
"GET",
558+
self.url + "/" + user1,
559+
access_token=self.admin_user_tok,
560+
)
561+
self.assertTrue(
562+
channel.json_body["last_seen_ts"] > 40000 + LAST_SEEN_GRANULARITY
563+
)
564+
565+
self._order_test([self.admin_user, user2, user3, user1], "last_seen_ts")
566+
509567
def test_search_term(self) -> None:
510568
"""Test that searching for a users works correctly"""
511569

@@ -1135,6 +1193,7 @@ def _check_fields(self, content: List[JsonDict]) -> None:
11351193
self.assertIn("displayname", u)
11361194
self.assertIn("avatar_url", u)
11371195
self.assertIn("creation_ts", u)
1196+
self.assertIn("last_seen_ts", u)
11381197

11391198
def _create_users(self, number_users: int) -> None:
11401199
"""
@@ -3035,6 +3094,7 @@ def _check_fields(self, content: JsonDict) -> None:
30353094
self.assertIn("consent_version", content)
30363095
self.assertIn("consent_ts", content)
30373096
self.assertIn("external_ids", content)
3097+
self.assertIn("last_seen_ts", content)
30383098

30393099
# This key was removed intentionally. Ensure it is not accidentally re-included.
30403100
self.assertNotIn("password_hash", content)

tests/storage/test_registration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def test_register(self) -> None:
5151
"locked": 0,
5252
"shadow_banned": 0,
5353
"approved": 1,
54+
"last_seen_ts": None,
5455
},
5556
(self.get_success(self.store.get_user_by_id(self.user_id))),
5657
)

0 commit comments

Comments
 (0)