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

Commit

Permalink
Add a module API method to retrieve state from a room (#11204)
Browse files Browse the repository at this point in the history
  • Loading branch information
babolivier committed Nov 1, 2021
1 parent c9c1bed commit c82793a
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.d/11204.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a module API method to retrieve the current state of a room.
138 changes: 137 additions & 1 deletion synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@
List,
Optional,
Tuple,
Union,
)

import attr
import jinja2

from twisted.internet import defer
from twisted.web.resource import IResource

from synapse.events import EventBase
from synapse.events.presence_router import PresenceRouter
from synapse.http.client import SimpleHttpClient
from synapse.http.server import (
DirectServeHtmlResource,
Expand All @@ -42,10 +45,19 @@
from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.rest.client.login import LoginResponse
from synapse.storage.database import DatabasePool, LoggingTransaction
from synapse.storage.databases.main.roommember import ProfileInfo
from synapse.storage.state import StateFilter
from synapse.types import JsonDict, Requester, UserID, UserInfo, create_requester
from synapse.types import (
DomainSpecificString,
JsonDict,
Requester,
StateMap,
UserID,
UserInfo,
create_requester,
)
from synapse.util import Clock
from synapse.util.caches.descriptors import cached

Expand All @@ -57,6 +69,8 @@
are loaded into Synapse.
"""

PRESENCE_ALL_USERS = PresenceRouter.ALL_USERS

__all__ = [
"errors",
"make_deferred_yieldable",
Expand All @@ -70,11 +84,28 @@
"DirectServeHtmlResource",
"DirectServeJsonResource",
"ModuleApi",
"PRESENCE_ALL_USERS",
"LoginResponse",
"JsonDict",
"EventBase",
"StateMap",
]

logger = logging.getLogger(__name__)


@attr.s(auto_attribs=True)
class UserIpAndAgent:
"""
An IP address and user agent used by a user to connect to this homeserver.
"""

ip: str
user_agent: str
# The time at which this user agent/ip was last seen.
last_seen: int


class ModuleApi:
"""A proxy object that gets passed to various plugin modules so they
can register new users etc if necessary.
Expand Down Expand Up @@ -690,6 +721,111 @@ def read_templates(
(td for td in (self.custom_template_dir, custom_template_directory) if td),
)

def is_mine(self, id: Union[str, DomainSpecificString]) -> bool:
"""
Checks whether an ID (user id, room, ...) comes from this homeserver.
Added in Synapse v1.44.0.
Args:
id: any Matrix id (e.g. user id, room id, ...), either as a raw id,
e.g. string "@user:example.com" or as a parsed UserID, RoomID, ...
Returns:
True if id comes from this homeserver, False otherwise.
"""
if isinstance(id, DomainSpecificString):
return self._hs.is_mine(id)
else:
return self._hs.is_mine_id(id)

async def get_user_ip_and_agents(
self, user_id: str, since_ts: int = 0
) -> List[UserIpAndAgent]:
"""
Return the list of user IPs and agents for a user.
Added in Synapse v1.44.0.
Args:
user_id: the id of a user, local or remote
since_ts: a timestamp in seconds since the epoch,
or the epoch itself if not specified.
Returns:
The list of all UserIpAndAgent that the user has
used to connect to this homeserver since `since_ts`.
If the user is remote, this list is empty.
"""
# Don't hit the db if this is not a local user.
is_mine = False
try:
# Let's be defensive against ill-formed strings.
if self.is_mine(user_id):
is_mine = True
except Exception:
pass

if is_mine:
raw_data = await self._store.get_user_ip_and_agents(
UserID.from_string(user_id), since_ts
)
# Sanitize some of the data. We don't want to return tokens.
return [
UserIpAndAgent(
ip=data["ip"],
user_agent=data["user_agent"],
last_seen=data["last_seen"],
)
for data in raw_data
]
else:
return []

async def get_room_state(
self,
room_id: str,
event_filter: Optional[Iterable[Tuple[str, Optional[str]]]] = None,
) -> StateMap[EventBase]:
"""Returns the current state of the given room.
The events are returned as a mapping, in which the key for each event is a tuple
which first element is the event's type and the second one is its state key.
Added in Synapse v1.47.0
Args:
room_id: The ID of the room to get state from.
event_filter: A filter to apply when retrieving events. None if no filter
should be applied. If provided, must be an iterable of tuples. A tuple's
first element is the event type and the second is the state key, or is
None if the state key should not be filtered on.
An example of a filter is:
[
("m.room.member", "@alice:example.com"), # Member event for @alice:example.com
("org.matrix.some_event", ""), # State event of type "org.matrix.some_event"
# with an empty string as its state key
("org.matrix.some_other_event", None), # State events of type "org.matrix.some_other_event"
# regardless of their state key
]
"""
if event_filter:
# If a filter was provided, turn it into a StateFilter and retrieve a filtered
# view of the state.
state_filter = StateFilter.from_types(event_filter)
state_ids = await self._store.get_filtered_current_state_ids(
room_id,
state_filter,
)
else:
# If no filter was provided, get the whole state. We could also reuse the call
# to get_filtered_current_state_ids above, with `state_filter = StateFilter.all()`,
# but get_filtered_current_state_ids isn't cached and `get_current_state_ids`
# is, so using the latter when we can is better for perf.
state_ids = await self._store.get_current_state_ids(room_id)

state_events = await self._store.get_events(state_ids.values())

return {key: state_events[event_id] for key, event_id in state_ids.items()}


class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
148 changes: 147 additions & 1 deletion tests/module_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from twisted.internet import defer

from synapse.api.constants import EduTypes
from synapse.api.constants import EduTypes, EventTypes
from synapse.events import EventBase
from synapse.federation.units import Transaction
from synapse.handlers.presence import UserPresenceState
Expand Down Expand Up @@ -308,6 +308,152 @@ def test_send_local_online_presence_to_federation(self):

self.assertTrue(found_update)

def test_update_membership(self):
"""Tests that the module API can update the membership of a user in a room."""
peter = self.register_user("peter", "hackme")
lesley = self.register_user("lesley", "hackme")
tok = self.login("peter", "hackme")
lesley_tok = self.login("lesley", "hackme")

# Make peter create a public room.
room_id = self.helper.create_room_as(
room_creator=peter, is_public=True, tok=tok
)

# Set a profile for lesley.
channel = self.make_request(
method="PUT",
path="/_matrix/client/r0/profile/%s/displayname" % lesley,
content={"displayname": "Lesley May"},
access_token=lesley_tok,
)

self.assertEqual(channel.code, 200, channel.result)

channel = self.make_request(
method="PUT",
path="/_matrix/client/r0/profile/%s/avatar_url" % lesley,
content={"avatar_url": "some_url"},
access_token=lesley_tok,
)

self.assertEqual(channel.code, 200, channel.result)

# Make Peter invite Lesley to the room.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, lesley, room_id, "invite")
)
)

res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)

# Check the membership is correct.
self.assertEqual(res["membership"], "invite")

# Also check that the profile was correctly filled out, and that it's not
# Peter's.
self.assertEqual(res["displayname"], "Lesley May")
self.assertEqual(res["avatar_url"], "some_url")

# Make lesley join it.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(lesley, lesley, room_id, "join")
)
)

# Check that the membership of lesley in the room is "join".
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)

self.assertEqual(res["membership"], "join")

# Also check that the profile was correctly filled out.
self.assertEqual(res["displayname"], "Lesley May")
self.assertEqual(res["avatar_url"], "some_url")

# Make peter kick lesley from the room.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, lesley, room_id, "leave")
)
)

# Check that the membership of lesley in the room is "leave".
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)

self.assertEqual(res["membership"], "leave")

# Try to send a membership update from a non-local user and check that it fails.
d = defer.ensureDeferred(
self.module_api.update_room_membership(
"@nicolas:otherserver.com",
lesley,
room_id,
"invite",
)
)

self.get_failure(d, RuntimeError)

# Check that inviting a user that doesn't have a profile falls back to using a
# default (localpart + no avatar) profile.
simone = "@simone:" + self.hs.config.server.server_name
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, simone, room_id, "invite")
)
)

res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=simone,
tok=tok,
)

self.assertEqual(res["membership"], "invite")
self.assertEqual(res["displayname"], "simone")
self.assertIsNone(res["avatar_url"])

def test_get_room_state(self):
"""Tests that a module can retrieve the state of a room through the module API."""
user_id = self.register_user("peter", "hackme")
tok = self.login("peter", "hackme")

# Create a room and send some custom state in it.
room_id = self.helper.create_room_as(tok=tok)
self.helper.send_state(room_id, "org.matrix.test", {}, tok=tok)

# Check that the module API can successfully fetch state for the room.
state = self.get_success(
defer.ensureDeferred(self.module_api.get_room_state(room_id))
)

# Check that a few standard events are in the returned state.
self.assertIn((EventTypes.Create, ""), state)
self.assertIn((EventTypes.Member, user_id), state)

# Check that our custom state event is in the returned state.
self.assertEqual(state[("org.matrix.test", "")].sender, user_id)
self.assertEqual(state[("org.matrix.test", "")].state_key, "")
self.assertEqual(state[("org.matrix.test", "")].content, {})


class ModuleApiWorkerTestCase(BaseMultiWorkerStreamTestCase):
"""For testing ModuleApi functionality in a multi-worker setup"""
Expand Down

0 comments on commit c82793a

Please sign in to comment.