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 all 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
32 changes: 32 additions & 0 deletions synapse/storage/databases/main/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ 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
"""

clause, args = make_in_list_sql_clause(
txn.database_engine, "user_id", user_ids
)
clokep marked this conversation as resolved.
Show resolved Hide resolved

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.
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