forked from MirrorHub/synapse
Add number of local devices to Room Details Admin API (#8886)
This commit is contained in:
parent
1d55c7b567
commit
0a34cdfc66
6 changed files with 138 additions and 27 deletions
1
changelog.d/8886.feature
Normal file
1
changelog.d/8886.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add number of local devices to Room Details Admin API. Contributed by @dklimpel.
|
|
@ -87,7 +87,7 @@ GET /_synapse/admin/v1/rooms
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```jsonc
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
|
@ -139,7 +139,7 @@ GET /_synapse/admin/v1/rooms?search_term=TWIM
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
|
@ -174,7 +174,7 @@ GET /_synapse/admin/v1/rooms?order_by=size
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```jsonc
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
|
@ -230,14 +230,14 @@ GET /_synapse/admin/v1/rooms?order_by=size&from=100
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```jsonc
|
||||
{
|
||||
"rooms": [
|
||||
{
|
||||
"room_id": "!mscvqgqpHYjBGDxNym:matrix.org",
|
||||
"name": "Music Theory",
|
||||
"canonical_alias": "#musictheory:matrix.org",
|
||||
"joined_members": 127
|
||||
"joined_members": 127,
|
||||
"joined_local_members": 2,
|
||||
"version": "1",
|
||||
"creator": "@foo:matrix.org",
|
||||
|
@ -254,7 +254,7 @@ Response:
|
|||
"room_id": "!twcBhHVdZlQWuuxBhN:termina.org.uk",
|
||||
"name": "weechat-matrix",
|
||||
"canonical_alias": "#weechat-matrix:termina.org.uk",
|
||||
"joined_members": 137
|
||||
"joined_members": 137,
|
||||
"joined_local_members": 20,
|
||||
"version": "4",
|
||||
"creator": "@foo:termina.org.uk",
|
||||
|
@ -289,6 +289,7 @@ The following fields are possible in the JSON response body:
|
|||
* `canonical_alias` - The canonical (main) alias address of the room.
|
||||
* `joined_members` - How many users are currently in the room.
|
||||
* `joined_local_members` - How many local users are currently in the room.
|
||||
* `joined_local_devices` - How many local devices are currently in the room.
|
||||
* `version` - The version of the room as a string.
|
||||
* `creator` - The `user_id` of the room creator.
|
||||
* `encryption` - Algorithm of end-to-end encryption of messages. Is `null` if encryption is not active.
|
||||
|
@ -311,15 +312,16 @@ GET /_synapse/admin/v1/rooms/<room_id>
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"room_id": "!mscvqgqpHYjBGDxNym:matrix.org",
|
||||
"name": "Music Theory",
|
||||
"avatar": "mxc://matrix.org/AQDaVFlbkQoErdOgqWRgiGSV",
|
||||
"topic": "Theory, Composition, Notation, Analysis",
|
||||
"canonical_alias": "#musictheory:matrix.org",
|
||||
"joined_members": 127
|
||||
"joined_members": 127,
|
||||
"joined_local_members": 2,
|
||||
"joined_local_devices": 2,
|
||||
"version": "1",
|
||||
"creator": "@foo:matrix.org",
|
||||
"encryption": null,
|
||||
|
@ -353,13 +355,13 @@ GET /_synapse/admin/v1/rooms/<room_id>/members
|
|||
|
||||
Response:
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"members": [
|
||||
"@foo:matrix.org",
|
||||
"@bar:matrix.org",
|
||||
"@foobar:matrix.org
|
||||
],
|
||||
"@foobar:matrix.org"
|
||||
],
|
||||
"total": 3
|
||||
}
|
||||
```
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# limitations under the License.
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from synapse.api.constants import EventTypes, JoinRules
|
||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||
|
@ -25,13 +25,17 @@ from synapse.http.servlet import (
|
|||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.admin._base import (
|
||||
admin_patterns,
|
||||
assert_requester_is_admin,
|
||||
assert_user_is_admin,
|
||||
)
|
||||
from synapse.storage.databases.main.room import RoomSortOrder
|
||||
from synapse.types import RoomAlias, RoomID, UserID, create_requester
|
||||
from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -45,12 +49,14 @@ class ShutdownRoomRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/shutdown_room/(?P<room_id>[^/]+)")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||
|
||||
async def on_POST(self, request, room_id):
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
|
@ -86,13 +92,15 @@ class DeleteRoomRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/delete$")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
|
||||
async def on_POST(self, request, room_id):
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
|
@ -146,12 +154,12 @@ class ListRoomRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/rooms$")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.store = hs.get_datastore()
|
||||
self.auth = hs.get_auth()
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
|
||||
async def on_GET(self, request):
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
|
@ -236,19 +244,24 @@ class RoomRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)$")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
await assert_requester_is_admin(self.auth, request)
|
||||
|
||||
ret = await self.store.get_room_with_stats(room_id)
|
||||
if not ret:
|
||||
raise NotFoundError("Room not found")
|
||||
|
||||
return 200, ret
|
||||
members = await self.store.get_users_in_room(room_id)
|
||||
ret["joined_local_devices"] = await self.store.count_devices_by_users(members)
|
||||
|
||||
return (200, ret)
|
||||
|
||||
|
||||
class RoomMembersRestServlet(RestServlet):
|
||||
|
@ -258,12 +271,14 @@ class RoomMembersRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/members")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def on_GET(self, request, room_id):
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest, room_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
await assert_requester_is_admin(self.auth, request)
|
||||
|
||||
ret = await self.store.get_room(room_id)
|
||||
|
@ -280,14 +295,16 @@ class JoinRoomAliasServlet(RestServlet):
|
|||
|
||||
PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
|
||||
async def on_POST(self, request, room_identifier):
|
||||
async def on_POST(
|
||||
self, request: SynapseRequest, room_identifier: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
|
@ -314,7 +331,6 @@ class JoinRoomAliasServlet(RestServlet):
|
|||
handler = self.room_member_handler
|
||||
room_alias = RoomAlias.from_string(room_identifier)
|
||||
room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias)
|
||||
room_id = room_id.to_string()
|
||||
else:
|
||||
raise SynapseError(
|
||||
400, "%s was not legal room ID or room alias" % (room_identifier,)
|
||||
|
|
|
@ -57,6 +57,38 @@ class DeviceWorkerStore(SQLBaseStore):
|
|||
self._prune_old_outbound_device_pokes, 60 * 60 * 1000
|
||||
)
|
||||
|
||||
async def count_devices_by_users(self, user_ids: Optional[List[str]] = None) -> int:
|
||||
"""Retrieve number of all devices of given users.
|
||||
Only returns number of devices that are not marked as hidden.
|
||||
|
||||
Args:
|
||||
user_ids: The IDs of the users which owns devices
|
||||
Returns:
|
||||
Number of devices of this users.
|
||||
"""
|
||||
|
||||
def count_devices_by_users_txn(txn, user_ids):
|
||||
sql = """
|
||||
SELECT count(*)
|
||||
FROM devices
|
||||
WHERE
|
||||
hidden = '0' AND
|
||||
"""
|
||||
|
||||
clause, args = make_in_list_sql_clause(
|
||||
txn.database_engine, "user_id", user_ids
|
||||
)
|
||||
|
||||
txn.execute(sql + clause, args)
|
||||
return txn.fetchone()[0]
|
||||
|
||||
if not user_ids:
|
||||
return 0
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"count_devices_by_users", count_devices_by_users_txn, user_ids
|
||||
)
|
||||
|
||||
async def get_device(self, user_id: str, device_id: str) -> Dict[str, Any]:
|
||||
"""Retrieve a device. Only returns devices that are not marked as
|
||||
hidden.
|
||||
|
|
|
@ -1084,6 +1084,7 @@ class RoomTestCase(unittest.HomeserverTestCase):
|
|||
self.assertIn("canonical_alias", channel.json_body)
|
||||
self.assertIn("joined_members", channel.json_body)
|
||||
self.assertIn("joined_local_members", channel.json_body)
|
||||
self.assertIn("joined_local_devices", channel.json_body)
|
||||
self.assertIn("version", channel.json_body)
|
||||
self.assertIn("creator", channel.json_body)
|
||||
self.assertIn("encryption", channel.json_body)
|
||||
|
@ -1096,6 +1097,39 @@ class RoomTestCase(unittest.HomeserverTestCase):
|
|||
|
||||
self.assertEqual(room_id_1, channel.json_body["room_id"])
|
||||
|
||||
def test_single_room_devices(self):
|
||||
"""Test that `joined_local_devices` can be requested correctly"""
|
||||
room_id_1 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
|
||||
|
||||
url = "/_synapse/admin/v1/rooms/%s" % (room_id_1,)
|
||||
request, channel = self.make_request(
|
||||
"GET", url.encode("ascii"), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(1, channel.json_body["joined_local_devices"])
|
||||
|
||||
# Have another user join the room
|
||||
user_1 = self.register_user("foo", "pass")
|
||||
user_tok_1 = self.login("foo", "pass")
|
||||
self.helper.join(room_id_1, user_1, tok=user_tok_1)
|
||||
|
||||
url = "/_synapse/admin/v1/rooms/%s" % (room_id_1,)
|
||||
request, channel = self.make_request(
|
||||
"GET", url.encode("ascii"), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(2, channel.json_body["joined_local_devices"])
|
||||
|
||||
# leave room
|
||||
self.helper.leave(room_id_1, self.admin_user, tok=self.admin_user_tok)
|
||||
self.helper.leave(room_id_1, user_1, tok=user_tok_1)
|
||||
url = "/_synapse/admin/v1/rooms/%s" % (room_id_1,)
|
||||
request, channel = self.make_request(
|
||||
"GET", url.encode("ascii"), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(0, channel.json_body["joined_local_devices"])
|
||||
|
||||
def test_room_members(self):
|
||||
"""Test that room members can be requested correctly"""
|
||||
# Create two test rooms
|
||||
|
|
|
@ -79,6 +79,32 @@ class DeviceStoreTestCase(tests.unittest.TestCase):
|
|||
res["device2"],
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_count_devices_by_users(self):
|
||||
yield defer.ensureDeferred(
|
||||
self.store.store_device("user_id", "device1", "display_name 1")
|
||||
)
|
||||
yield defer.ensureDeferred(
|
||||
self.store.store_device("user_id", "device2", "display_name 2")
|
||||
)
|
||||
yield defer.ensureDeferred(
|
||||
self.store.store_device("user_id2", "device3", "display_name 3")
|
||||
)
|
||||
|
||||
res = yield defer.ensureDeferred(self.store.count_devices_by_users())
|
||||
self.assertEqual(0, res)
|
||||
|
||||
res = yield defer.ensureDeferred(self.store.count_devices_by_users(["unknown"]))
|
||||
self.assertEqual(0, res)
|
||||
|
||||
res = yield defer.ensureDeferred(self.store.count_devices_by_users(["user_id"]))
|
||||
self.assertEqual(2, res)
|
||||
|
||||
res = yield defer.ensureDeferred(
|
||||
self.store.count_devices_by_users(["user_id", "user_id2"])
|
||||
)
|
||||
self.assertEqual(3, res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_device_updates_by_remote(self):
|
||||
device_ids = ["device_id1", "device_id2"]
|
||||
|
|
Loading…
Reference in a new issue