Add /user/{user_id}/shared_rooms/ api (#7785)

* Add shared_rooms api

* Add changelog

* Add .

* Wrap response in {"rooms": }

* linting

* Add unstable_features key

* Remove options from isort that aren't part of 5.x

`-y` and `-rc` are now default behaviour and no longer exist.

`dont-skip` is no longer required

https://timothycrosley.github.io/isort/CHANGELOG/#500-penny-july-4-2020

* Update imports to make isort happy

* Add changelog

* Update tox.ini file with correct invocation

* fix linting again for isort

* Vendor prefix unstable API

* Fix to match spec

* import Codes

* import Codes

* Use FORBIDDEN

* Update changelog.d/7785.feature

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>

* Implement get_shared_rooms_for_users

* a comma

* trailing whitespace

* Handle the easy feedback

* Switch to using runInteraction

* Add tests

* Feedback

* Seperate unstable endpoint from v2

* Add upgrade node

* a line

* Fix style by adding a blank line at EOF.

* Update synapse/storage/databases/main/user_directory.py

Co-authored-by: Tulir Asokan <tulir@maunium.net>

* Update synapse/storage/databases/main/user_directory.py

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>

* Update UPGRADE.rst

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>

* Fix UPGRADE/CHANGELOG unstable paths

unstable unstable unstable

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
Co-authored-by: Tulir Asokan <tulir@maunium.net>

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
Co-authored-by: Patrick Cloke <clokep@users.noreply.github.com>
Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
Will Hunt 2020-09-02 13:18:40 +01:00 committed by GitHub
parent 9356656e67
commit b257c788c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 1 deletions

View File

@ -1,3 +1,16 @@
Upgrading to v1.20.0
====================
Shared rooms endpoint (MSC2666)
-------------------------------
This release contains a new unstable endpoint `/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/.*`
for fetching rooms one user has in common with another. This feature requires the
`update_user_directory` config flag to be `True`. If you are you are using a `synapse.app.user_dir`
worker, requests to this endpoint must be handled by that worker.
See `docs/workers.md <docs/workers.md>`_ for more details.
Upgrading Synapse Upgrading Synapse
================= =================

1
changelog.d/7785.feature Normal file
View File

@ -0,0 +1 @@
Add an endpoint to query your shared rooms with another user as an implementation of [MSC2666](https://github.com/matrix-org/matrix-doc/pull/2666).

View File

@ -380,6 +380,7 @@ Handles searches in the user directory. It can handle REST endpoints matching
the following regular expressions: the following regular expressions:
^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$ ^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$
^/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/.*$
When using this worker you must also set `update_user_directory: False` in the When using this worker you must also set `update_user_directory: False` in the
shared configuration file to stop the main synapse running background shared configuration file to stop the main synapse running background

View File

@ -50,6 +50,7 @@ from synapse.rest.client.v2_alpha import (
room_keys, room_keys,
room_upgrade_rest_servlet, room_upgrade_rest_servlet,
sendtodevice, sendtodevice,
shared_rooms,
sync, sync,
tags, tags,
thirdparty, thirdparty,
@ -125,3 +126,6 @@ class ClientRestResource(JsonResource):
synapse.rest.admin.register_servlets_for_client_rest_resource( synapse.rest.admin.register_servlets_for_client_rest_resource(
hs, client_resource hs, client_resource
) )
# unstable
shared_rooms.register_servlets(hs, client_resource)

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Half-Shot
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import RestServlet
from synapse.types import UserID
from ._base import client_patterns
logger = logging.getLogger(__name__)
class UserSharedRoomsServlet(RestServlet):
"""
GET /uk.half-shot.msc2666/user/shared_rooms/{user_id} HTTP/1.1
"""
PATTERNS = client_patterns(
"/uk.half-shot.msc2666/user/shared_rooms/(?P<user_id>[^/]*)",
releases=(), # This is an unstable feature
)
def __init__(self, hs):
super(UserSharedRoomsServlet, self).__init__()
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.user_directory_active = hs.config.update_user_directory
async def on_GET(self, request, user_id):
if not self.user_directory_active:
raise SynapseError(
code=400,
msg="The user directory is disabled on this server. Cannot determine shared rooms.",
errcode=Codes.FORBIDDEN,
)
UserID.from_string(user_id)
requester = await self.auth.get_user_by_req(request)
if user_id == requester.user.to_string():
raise SynapseError(
code=400,
msg="You cannot request a list of shared rooms with yourself",
errcode=Codes.FORBIDDEN,
)
rooms = await self.store.get_shared_rooms_for_users(
requester.user.to_string(), user_id
)
return 200, {"joined": list(rooms)}
def register_servlets(hs, http_server):
UserSharedRoomsServlet(hs).register(http_server)

View File

@ -60,6 +60,8 @@ class VersionsRestServlet(RestServlet):
"org.matrix.e2e_cross_signing": True, "org.matrix.e2e_cross_signing": True,
# Implements additional endpoints as described in MSC2432 # Implements additional endpoints as described in MSC2432
"org.matrix.msc2432": True, "org.matrix.msc2432": True,
# Implements additional endpoints as described in MSC2666
"uk.half-shot.msc2666": True,
}, },
}, },
) )

View File

@ -15,7 +15,7 @@
import logging import logging
import re import re
from typing import Any, Dict, Iterable, Optional, Tuple from typing import Any, Dict, Iterable, Optional, Set, Tuple
from synapse.api.constants import EventTypes, JoinRules from synapse.api.constants import EventTypes, JoinRules
from synapse.storage.database import DatabasePool from synapse.storage.database import DatabasePool
@ -675,6 +675,48 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
users.update(rows) users.update(rows)
return list(users) return list(users)
@cached()
async def get_shared_rooms_for_users(
self, user_id: str, other_user_id: str
) -> Set[str]:
"""
Returns the rooms that a local user shares with another local or remote user.
Args:
user_id: The MXID of a local user
other_user_id: The MXID of the other user
Returns:
A set of room ID's that the users share.
"""
def _get_shared_rooms_for_users_txn(txn):
txn.execute(
"""
SELECT p1.room_id
FROM users_in_public_rooms as p1
INNER JOIN users_in_public_rooms as p2
ON p1.room_id = p2.room_id
AND p1.user_id = ?
AND p2.user_id = ?
UNION
SELECT room_id
FROM users_who_share_private_rooms
WHERE
user_id = ?
AND other_user_id = ?
""",
(user_id, other_user_id, user_id, other_user_id),
)
rows = self.db_pool.cursor_to_dict(txn)
return rows
rows = await self.db_pool.runInteraction(
"get_shared_rooms_for_users", _get_shared_rooms_for_users_txn
)
return {row["room_id"] for row in rows}
async def get_user_directory_stream_pos(self) -> int: async def get_user_directory_stream_pos(self) -> int:
return await self.db_pool.simple_select_one_onecol( return await self.db_pool.simple_select_one_onecol(
table="user_directory_stream_pos", table="user_directory_stream_pos",

View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# Copyright 2020 Half-Shot
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import synapse.rest.admin
from synapse.rest.client.v1 import login, room
from synapse.rest.client.v2_alpha import shared_rooms
from tests import unittest
class UserSharedRoomsTest(unittest.HomeserverTestCase):
"""
Tests the UserSharedRoomsServlet.
"""
servlets = [
login.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
shared_rooms.register_servlets,
]
def make_homeserver(self, reactor, clock):
config = self.default_config()
config["update_user_directory"] = True
return self.setup_test_homeserver(config=config)
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.handler = hs.get_user_directory_handler()
def _get_shared_rooms(self, token, other_user):
request, channel = self.make_request(
"GET",
"/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/%s"
% other_user,
access_token=token,
)
self.render(request)
return request, channel
def test_shared_room_list_public(self):
"""
A room should show up in the shared list of rooms between two users
if it is public.
"""
u1 = self.register_user("user1", "pass")
u1_token = self.login(u1, "pass")
u2 = self.register_user("user2", "pass")
u2_token = self.login(u2, "pass")
room = self.helper.create_room_as(u1, is_public=True, tok=u1_token)
self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
self.helper.join(room, user=u2, tok=u2_token)
request, channel = self._get_shared_rooms(u1_token, u2)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(len(channel.json_body["joined"]), 1)
self.assertEquals(channel.json_body["joined"][0], room)
def test_shared_room_list_private(self):
"""
A room should show up in the shared list of rooms between two users
if it is private.
"""
u1 = self.register_user("user1", "pass")
u1_token = self.login(u1, "pass")
u2 = self.register_user("user2", "pass")
u2_token = self.login(u2, "pass")
room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
self.helper.join(room, user=u2, tok=u2_token)
request, channel = self._get_shared_rooms(u1_token, u2)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(len(channel.json_body["joined"]), 1)
self.assertEquals(channel.json_body["joined"][0], room)
def test_shared_room_list_mixed(self):
"""
The shared room list between two users should contain both public and private
rooms.
"""
u1 = self.register_user("user1", "pass")
u1_token = self.login(u1, "pass")
u2 = self.register_user("user2", "pass")
u2_token = self.login(u2, "pass")
room_public = self.helper.create_room_as(u1, is_public=True, tok=u1_token)
room_private = self.helper.create_room_as(u2, is_public=False, tok=u2_token)
self.helper.invite(room_public, src=u1, targ=u2, tok=u1_token)
self.helper.invite(room_private, src=u2, targ=u1, tok=u2_token)
self.helper.join(room_public, user=u2, tok=u2_token)
self.helper.join(room_private, user=u1, tok=u1_token)
request, channel = self._get_shared_rooms(u1_token, u2)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(len(channel.json_body["joined"]), 2)
self.assertTrue(room_public in channel.json_body["joined"])
self.assertTrue(room_private in channel.json_body["joined"])
def test_shared_room_list_after_leave(self):
"""
A room should no longer be considered shared if the other
user has left it.
"""
u1 = self.register_user("user1", "pass")
u1_token = self.login(u1, "pass")
u2 = self.register_user("user2", "pass")
u2_token = self.login(u2, "pass")
room = self.helper.create_room_as(u1, is_public=True, tok=u1_token)
self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
self.helper.join(room, user=u2, tok=u2_token)
# Assert user directory is not empty
request, channel = self._get_shared_rooms(u1_token, u2)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(len(channel.json_body["joined"]), 1)
self.assertEquals(channel.json_body["joined"][0], room)
self.helper.leave(room, user=u1, tok=u1_token)
request, channel = self._get_shared_rooms(u2_token, u1)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(len(channel.json_body["joined"]), 0)