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

Add number of local devices to Room Details Admin API #8886

Merged
merged 5 commits into from
Dec 11, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8886.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add number of local devices to Room Details Admin API. Contributed by @dklimpel.
24 changes: 13 additions & 11 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ GET /_synapse/admin/v1/rooms

Response:

```
```jsonc
clokep marked this conversation as resolved.
Show resolved Hide resolved
{
"rooms": [
{
Expand Down Expand Up @@ -128,7 +128,7 @@ GET /_synapse/admin/v1/rooms?search_term=TWIM

Response:

```
```json
{
"rooms": [
{
Expand Down Expand Up @@ -163,7 +163,7 @@ GET /_synapse/admin/v1/rooms?order_by=size

Response:

```
```jsonc
{
"rooms": [
{
Expand Down Expand Up @@ -219,14 +219,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",
Expand All @@ -243,7 +243,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",
Expand Down Expand Up @@ -278,6 +278,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.
Expand All @@ -300,15 +301,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,
Expand Down Expand Up @@ -342,13 +344,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
}
```
Expand Down
48 changes: 32 additions & 16 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,13 +25,17 @@
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__)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -314,7 +331,6 @@ async def on_POST(self, request, room_identifier):
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,)
Expand Down
31 changes: 31 additions & 0 deletions synapse/storage/databases/main/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ def __init__(self, database: DatabasePool, db_conn, hs):
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
user_id IN ({})
""".format(
",".join("?" for _ in user_ids)
)
clokep marked this conversation as resolved.
Show resolved Hide resolved

txn.execute(sql, user_ids)
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.
Expand Down
34 changes: 34 additions & 0 deletions tests/rest/admin/test_room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,7 @@ def test_single_room(self):
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)
Expand All @@ -1096,6 +1097,39 @@ def test_single_room(self):

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
Expand Down
26 changes: 26 additions & 0 deletions tests/storage/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,32 @@ def test_get_devices_by_user(self):
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"]
Expand Down