mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-13 23:43:45 +01:00
Add last_seen_ts to the admin users API (#16218)
This commit is contained in:
parent
721346631e
commit
dcb2778341
10 changed files with 80 additions and 2 deletions
1
changelog.d/16218.feature
Normal file
1
changelog.d/16218.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add `last_seen_ts` to the admin users API.
|
|
@ -242,6 +242,7 @@ The following parameters should be set in the URL:
|
||||||
- `displayname` - Users are ordered alphabetically by `displayname`.
|
- `displayname` - Users are ordered alphabetically by `displayname`.
|
||||||
- `avatar_url` - Users are ordered alphabetically by avatar URL.
|
- `avatar_url` - Users are ordered alphabetically by avatar URL.
|
||||||
- `creation_ts` - Users are ordered by when the users was created in ms.
|
- `creation_ts` - Users are ordered by when the users was created in ms.
|
||||||
|
- `last_seen_ts` - Users are ordered by when the user was lastly seen in ms.
|
||||||
|
|
||||||
- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards.
|
- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards.
|
||||||
Setting this value to `b` will reverse the above sort order. Defaults to `f`.
|
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:
|
||||||
- `displayname` - string - The user's display name if they have set one.
|
- `displayname` - string - The user's display name if they have set one.
|
||||||
- `avatar_url` - string - The user's avatar URL if they have set one.
|
- `avatar_url` - string - The user's avatar URL if they have set one.
|
||||||
- `creation_ts` - integer - The user's creation timestamp in ms.
|
- `creation_ts` - integer - The user's creation timestamp in ms.
|
||||||
|
- `last_seen_ts` - integer - The user's last activity timestamp in ms.
|
||||||
|
|
||||||
- `next_token`: string representing a positive integer - Indication for pagination. See above.
|
- `next_token`: string representing a positive integer - Indication for pagination. See above.
|
||||||
- `total` - integer - Total number of media.
|
- `total` - integer - Total number of media.
|
||||||
|
|
|
@ -76,6 +76,7 @@ class AdminHandler:
|
||||||
"consent_ts",
|
"consent_ts",
|
||||||
"user_type",
|
"user_type",
|
||||||
"is_guest",
|
"is_guest",
|
||||||
|
"last_seen_ts",
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._msc3866_enabled:
|
if self._msc3866_enabled:
|
||||||
|
|
|
@ -132,6 +132,7 @@ class UsersRestServletV2(RestServlet):
|
||||||
UserSortOrder.AVATAR_URL.value,
|
UserSortOrder.AVATAR_URL.value,
|
||||||
UserSortOrder.SHADOW_BANNED.value,
|
UserSortOrder.SHADOW_BANNED.value,
|
||||||
UserSortOrder.CREATION_TS.value,
|
UserSortOrder.CREATION_TS.value,
|
||||||
|
UserSortOrder.LAST_SEEN_TS.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -277,6 +277,10 @@ class DataStore(
|
||||||
FROM users as u
|
FROM users as u
|
||||||
LEFT JOIN profiles AS p ON u.name = p.full_user_id
|
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 erased_users AS eu ON u.name = eu.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
|
||||||
{where_clause}
|
{where_clause}
|
||||||
"""
|
"""
|
||||||
sql = "SELECT COUNT(*) as total_users " + sql_base
|
sql = "SELECT COUNT(*) as total_users " + sql_base
|
||||||
|
@ -286,7 +290,7 @@ class DataStore(
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
|
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
|
||||||
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved,
|
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved,
|
||||||
eu.user_id is not null as erased
|
eu.user_id is not null as erased, last_seen_ts
|
||||||
{sql_base}
|
{sql_base}
|
||||||
ORDER BY {order_by_column} {order}, u.name ASC
|
ORDER BY {order_by_column} {order}, u.name ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
|
|
|
@ -206,8 +206,12 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
consent_server_notice_sent, appservice_id, creation_ts, user_type,
|
consent_server_notice_sent, appservice_id, creation_ts, user_type,
|
||||||
deactivated, COALESCE(shadow_banned, FALSE) AS shadow_banned,
|
deactivated, COALESCE(shadow_banned, FALSE) AS shadow_banned,
|
||||||
COALESCE(approved, TRUE) AS approved,
|
COALESCE(approved, TRUE) AS approved,
|
||||||
COALESCE(locked, FALSE) AS locked
|
COALESCE(locked, FALSE) AS locked, last_seen_ts
|
||||||
FROM users
|
FROM users
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT user_id, MAX(last_seen) AS last_seen_ts
|
||||||
|
FROM user_ips GROUP BY user_id
|
||||||
|
) ls ON users.name = ls.user_id
|
||||||
WHERE name = ?
|
WHERE name = ?
|
||||||
""",
|
""",
|
||||||
(user_id,),
|
(user_id,),
|
||||||
|
@ -268,6 +272,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
is_shadow_banned=bool(user_data["shadow_banned"]),
|
is_shadow_banned=bool(user_data["shadow_banned"]),
|
||||||
user_id=UserID.from_string(user_data["name"]),
|
user_id=UserID.from_string(user_data["name"]),
|
||||||
user_type=user_data["user_type"],
|
user_type=user_data["user_type"],
|
||||||
|
last_seen_ts=user_data["last_seen_ts"],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def is_trial_user(self, user_id: str) -> bool:
|
async def is_trial_user(self, user_id: str) -> bool:
|
||||||
|
|
|
@ -107,6 +107,7 @@ class UserSortOrder(Enum):
|
||||||
AVATAR_URL = "avatar_url"
|
AVATAR_URL = "avatar_url"
|
||||||
SHADOW_BANNED = "shadow_banned"
|
SHADOW_BANNED = "shadow_banned"
|
||||||
CREATION_TS = "creation_ts"
|
CREATION_TS = "creation_ts"
|
||||||
|
LAST_SEEN_TS = "last_seen_ts"
|
||||||
|
|
||||||
|
|
||||||
class StatsStore(StateDeltasStore):
|
class StatsStore(StateDeltasStore):
|
||||||
|
|
|
@ -946,6 +946,7 @@ class UserInfo:
|
||||||
is_guest: True if the user is a guest user.
|
is_guest: True if the user is a guest user.
|
||||||
is_shadow_banned: True if the user has been shadow-banned.
|
is_shadow_banned: True if the user has been shadow-banned.
|
||||||
user_type: User type (None for normal user, 'support' and 'bot' other options).
|
user_type: User type (None for normal user, 'support' and 'bot' other options).
|
||||||
|
last_seen_ts: Last activity timestamp of the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_id: UserID
|
user_id: UserID
|
||||||
|
@ -958,6 +959,7 @@ class UserInfo:
|
||||||
is_deactivated: bool
|
is_deactivated: bool
|
||||||
is_guest: bool
|
is_guest: bool
|
||||||
is_shadow_banned: bool
|
is_shadow_banned: bool
|
||||||
|
last_seen_ts: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(TypedDict):
|
class UserProfile(TypedDict):
|
||||||
|
|
|
@ -40,6 +40,7 @@ from synapse.rest.client import (
|
||||||
user_directory,
|
user_directory,
|
||||||
)
|
)
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
from synapse.storage.databases.main.client_ips import LAST_SEEN_GRANULARITY
|
||||||
from synapse.types import JsonDict, UserID, create_requester
|
from synapse.types import JsonDict, UserID, create_requester
|
||||||
from synapse.util import Clock
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
@ -456,6 +457,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [
|
servlets = [
|
||||||
synapse.rest.admin.register_servlets,
|
synapse.rest.admin.register_servlets,
|
||||||
login.register_servlets,
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
]
|
]
|
||||||
url = "/_synapse/admin/v2/users"
|
url = "/_synapse/admin/v2/users"
|
||||||
|
|
||||||
|
@ -506,6 +508,62 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
# Check that all fields are available
|
# Check that all fields are available
|
||||||
self._check_fields(channel.json_body["users"])
|
self._check_fields(channel.json_body["users"])
|
||||||
|
|
||||||
|
def test_last_seen(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that last_seen_ts field is properly working.
|
||||||
|
"""
|
||||||
|
user1 = self.register_user("u1", "pass")
|
||||||
|
user1_token = self.login("u1", "pass")
|
||||||
|
user2 = self.register_user("u2", "pass")
|
||||||
|
user2_token = self.login("u2", "pass")
|
||||||
|
user3 = self.register_user("u3", "pass")
|
||||||
|
user3_token = self.login("u3", "pass")
|
||||||
|
|
||||||
|
self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
|
||||||
|
self.reactor.advance(10)
|
||||||
|
self.helper.create_room_as(user2, tok=user2_token)
|
||||||
|
self.reactor.advance(10)
|
||||||
|
self.helper.create_room_as(user1, tok=user1_token)
|
||||||
|
self.reactor.advance(10)
|
||||||
|
self.helper.create_room_as(user3, tok=user3_token)
|
||||||
|
self.reactor.advance(10)
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
self.url,
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual(4, len(channel.json_body["users"]))
|
||||||
|
self.assertEqual(4, channel.json_body["total"])
|
||||||
|
|
||||||
|
admin_last_seen = channel.json_body["users"][0]["last_seen_ts"]
|
||||||
|
user1_last_seen = channel.json_body["users"][1]["last_seen_ts"]
|
||||||
|
user2_last_seen = channel.json_body["users"][2]["last_seen_ts"]
|
||||||
|
user3_last_seen = channel.json_body["users"][3]["last_seen_ts"]
|
||||||
|
self.assertTrue(admin_last_seen > 0 and admin_last_seen < 10000)
|
||||||
|
self.assertTrue(user2_last_seen > 10000 and user2_last_seen < 20000)
|
||||||
|
self.assertTrue(user1_last_seen > 20000 and user1_last_seen < 30000)
|
||||||
|
self.assertTrue(user3_last_seen > 30000 and user3_last_seen < 40000)
|
||||||
|
|
||||||
|
self._order_test([self.admin_user, user2, user1, user3], "last_seen_ts")
|
||||||
|
|
||||||
|
self.reactor.advance(LAST_SEEN_GRANULARITY / 1000)
|
||||||
|
self.helper.create_room_as(user1, tok=user1_token)
|
||||||
|
self.reactor.advance(10)
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
self.url + "/" + user1,
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
channel.json_body["last_seen_ts"] > 40000 + LAST_SEEN_GRANULARITY
|
||||||
|
)
|
||||||
|
|
||||||
|
self._order_test([self.admin_user, user2, user3, user1], "last_seen_ts")
|
||||||
|
|
||||||
def test_search_term(self) -> None:
|
def test_search_term(self) -> None:
|
||||||
"""Test that searching for a users works correctly"""
|
"""Test that searching for a users works correctly"""
|
||||||
|
|
||||||
|
@ -1135,6 +1193,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertIn("displayname", u)
|
self.assertIn("displayname", u)
|
||||||
self.assertIn("avatar_url", u)
|
self.assertIn("avatar_url", u)
|
||||||
self.assertIn("creation_ts", u)
|
self.assertIn("creation_ts", u)
|
||||||
|
self.assertIn("last_seen_ts", u)
|
||||||
|
|
||||||
def _create_users(self, number_users: int) -> None:
|
def _create_users(self, number_users: int) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -3035,6 +3094,7 @@ class UserRestTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertIn("consent_version", content)
|
self.assertIn("consent_version", content)
|
||||||
self.assertIn("consent_ts", content)
|
self.assertIn("consent_ts", content)
|
||||||
self.assertIn("external_ids", content)
|
self.assertIn("external_ids", content)
|
||||||
|
self.assertIn("last_seen_ts", content)
|
||||||
|
|
||||||
# This key was removed intentionally. Ensure it is not accidentally re-included.
|
# This key was removed intentionally. Ensure it is not accidentally re-included.
|
||||||
self.assertNotIn("password_hash", content)
|
self.assertNotIn("password_hash", content)
|
||||||
|
|
|
@ -51,6 +51,7 @@ class RegistrationStoreTestCase(HomeserverTestCase):
|
||||||
"locked": 0,
|
"locked": 0,
|
||||||
"shadow_banned": 0,
|
"shadow_banned": 0,
|
||||||
"approved": 1,
|
"approved": 1,
|
||||||
|
"last_seen_ts": None,
|
||||||
},
|
},
|
||||||
(self.get_success(self.store.get_user_by_id(self.user_id))),
|
(self.get_success(self.store.get_user_by_id(self.user_id))),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue