diff --git a/changelog.d/18967.feature b/changelog.d/18967.feature new file mode 100644 index 00000000000..58337d9e5db --- /dev/null +++ b/changelog.d/18967.feature @@ -0,0 +1 @@ +Add experimental implementation for the latest draft of [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143). \ No newline at end of file diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 3c401d569bb..6ee6d28de88 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -2575,6 +2575,28 @@ Example configuration: turn_allow_guests: false ``` --- +### `matrix_rtc` + +*(object)* Options related to MatrixRTC. Defaults to `{}`. + +This setting has the following sub-options: + +* `transports` (array): A list of transport types and arguments to use for MatrixRTC connections. Defaults to `[]`. + + Options for each entry include: + + * `type` (string): The type of transport to use to connect to the selective forwarding unit (SFU). + + * `livekit_service_url` (string): The base URL of the LiveKit service. Should only be used with LiveKit-based transports. + +Example configuration: +```yaml +matrix_rtc: + transports: + - type: livekit + livekit_service_url: https://matrix-rtc.example.com/livekit/jwt +``` +--- ## Registration Registration can be rate-limited using the parameters in the [Ratelimiting](#ratelimiting) section of this manual. diff --git a/schema/synapse-config.schema.yaml b/schema/synapse-config.schema.yaml index fdce4219ae4..98ebe5b623a 100644 --- a/schema/synapse-config.schema.yaml +++ b/schema/synapse-config.schema.yaml @@ -2886,6 +2886,35 @@ properties: default: true examples: - false + matrix_rtc: + type: object + description: >- + Options related to MatrixRTC. + properties: + transports: + type: array + items: + type: object + required: + - type + properties: + type: + type: string + description: The type of transport to use to connect to the selective forwarding unit (SFU). + example: livekit + livekit_service_url: + type: string + description: >- + The base URL of the LiveKit service. Should only be used with LiveKit-based transports. + example: https://matrix-rtc.example.com/livekit/jwt + description: + A list of transport types and arguments to use for MatrixRTC connections. + default: [] + default: {} + examples: + - transports: + - type: livekit + livekit_service_url: https://matrix-rtc.example.com/livekit/jwt enable_registration: type: boolean description: >- diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index 5e036352062..ed16d5b313e 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -37,6 +37,7 @@ from synapse.config import ( # noqa: F401 key, logger, mas, + matrixrtc, metrics, modules, oembed, @@ -126,6 +127,7 @@ class RootConfig: auto_accept_invites: auto_accept_invites.AutoAcceptInvitesConfig user_types: user_types.UserTypesConfig mas: mas.MasConfig + matrix_rtc: matrixrtc.MatrixRtcConfig config_classes: List[Type["Config"]] = ... config_files: List[str] diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index d7a3d675583..04ca6e3c517 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -556,6 +556,9 @@ def read_config( # MSC4133: Custom profile fields self.msc4133_enabled: bool = experimental.get("msc4133_enabled", False) + # MSC4143: Matrix RTC Transport using Livekit Backend + self.msc4143_enabled: bool = experimental.get("msc4143_enabled", False) + # MSC4169: Backwards-compatible redaction sending using `/send` self.msc4169_enabled: bool = experimental.get("msc4169_enabled", False) diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 5d7089c2e6a..f46f41da319 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -37,6 +37,7 @@ from .key import KeyConfig from .logger import LoggingConfig from .mas import MasConfig +from .matrixrtc import MatrixRtcConfig from .metrics import MetricsConfig from .modules import ModulesConfig from .oembed import OembedConfig @@ -80,6 +81,7 @@ class HomeServerConfig(RootConfig): OembedConfig, CaptchaConfig, VoipConfig, + MatrixRtcConfig, RegistrationConfig, AccountValidityConfig, MetricsConfig, diff --git a/synapse/config/matrixrtc.py b/synapse/config/matrixrtc.py new file mode 100644 index 00000000000..7844d8f398f --- /dev/null +++ b/synapse/config/matrixrtc.py @@ -0,0 +1,67 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +# [This file includes modifications made by New Vector Limited] +# +# + +from typing import Any, Optional + +from pydantic import ValidationError + +from synapse._pydantic_compat import Field, StrictStr, validator +from synapse.types import JsonDict +from synapse.util.pydantic_models import ParseModel + +from ._base import Config, ConfigError + + +class TransportConfigModel(ParseModel): + type: StrictStr + + livekit_service_url: Optional[StrictStr] = Field(default=None) + """An optional livekit service URL. Only required if type is "livekit".""" + + @validator("livekit_service_url", always=True) + def validate_livekit_service_url(cls, v: Any, values: dict) -> Any: + if values.get("type") == "livekit" and not v: + raise ValueError( + "You must set a `livekit_service_url` when using the 'livekit' transport." + ) + + return v + + +class MatrixRtcConfigModel(ParseModel): + transports: list = [] + + +class MatrixRtcConfig(Config): + section = "matrix_rtc" + + def read_config( + self, config: JsonDict, allow_secrets_in_config: bool, **kwargs: Any + ) -> None: + matrix_rtc = config.get("matrix_rtc", {}) + if matrix_rtc is None: + matrix_rtc = {} + + try: + parsed = MatrixRtcConfigModel(**matrix_rtc) + except ValidationError as e: + raise ConfigError( + "Could not validate matrix_rtc config", + ("matrix_rtc",), + ) from e + + self.transports = parsed.transports diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index a24ca098469..db3bd465425 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -42,6 +42,7 @@ login, login_token_request, logout, + matrixrtc, mutual_rooms, notifications, openid, @@ -89,6 +90,7 @@ presence.register_servlets, directory.register_servlets, voip.register_servlets, + matrixrtc.register_servlets, pusher.register_servlets, push_rule.register_servlets, logout.register_servlets, diff --git a/synapse/rest/client/matrixrtc.py b/synapse/rest/client/matrixrtc.py new file mode 100644 index 00000000000..afe4d4fa83e --- /dev/null +++ b/synapse/rest/client/matrixrtc.py @@ -0,0 +1,52 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +# [This file includes modifications made by New Vector Limited] +# +# + +from typing import TYPE_CHECKING, Tuple + +from synapse.http.server import HttpServer +from synapse.http.servlet import RestServlet +from synapse.http.site import SynapseRequest +from synapse.rest.client._base import client_patterns +from synapse.types import JsonDict + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +class MatrixRTCRestServlet(RestServlet): + PATTERNS = client_patterns(r"/org\.matrix\.msc4143/rtc/transports$", releases=()) + CATEGORY = "Client API requests" + + def __init__(self, hs: "HomeServer"): + super().__init__() + self._hs = hs + self._auth = hs.get_auth() + self._transports = hs.config.matrix_rtc.transports + + async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + # Require authentication for this endpoint. + await self._auth.get_user_by_req(request) + + if self._transports: + return 200, {"rtc_transports": self._transports} + + return 200, {} + + +def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: + if hs.config.experimental.msc4143_enabled: + MatrixRTCRestServlet(hs).register(http_server) diff --git a/tests/rest/client/test_matrixrtc.py b/tests/rest/client/test_matrixrtc.py new file mode 100644 index 00000000000..b5216c7adc8 --- /dev/null +++ b/tests/rest/client/test_matrixrtc.py @@ -0,0 +1,105 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +# [This file includes modifications made by New Vector Limited] +# +# + +"""Tests REST events for /rtc/endpoints path.""" + +from twisted.internet.testing import MemoryReactor + +from synapse.rest import admin +from synapse.rest.client import login, matrixrtc, register, room +from synapse.server import HomeServer +from synapse.util.clock import Clock + +from tests.unittest import HomeserverTestCase, override_config + +PATH_PREFIX = "/_matrix/client/unstable/org.matrix.msc4143" +RTC_ENDPOINT = {"type": "focusA", "required_field": "theField"} +LIVEKIT_ENDPOINT = { + "type": "livekit", + "livekit_service_url": "https://livekit.example.com", +} + + +class MatrixRtcTestCase(HomeserverTestCase): + """Tests /rtc/transports Client-Server REST API.""" + + servlets = [ + admin.register_servlets, + room.register_servlets, + login.register_servlets, + register.register_servlets, + matrixrtc.register_servlets, + ] + + def prepare( + self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer + ) -> None: + self.register_user("alice", "password") + self._alice_tok = self.login("alice", "password") + + def test_matrixrtc_endpoint_not_enabled(self) -> None: + channel = self.make_request( + "GET", f"{PATH_PREFIX}/rtc/transports", access_token=self._alice_tok + ) + self.assertEqual(404, channel.code, channel.json_body) + self.assertEqual( + "M_UNRECOGNIZED", channel.json_body["errcode"], channel.json_body + ) + + @override_config({"experimental_features": {"msc4143_enabled": True}}) + def test_matrixrtc_endpoint_requires_authentication(self) -> None: + channel = self.make_request("GET", f"{PATH_PREFIX}/rtc/transports") + self.assertEqual(401, channel.code, channel.json_body) + + @override_config( + { + "experimental_features": {"msc4143_enabled": True}, + "matrix_rtc": {"transports": [RTC_ENDPOINT]}, + } + ) + def test_matrixrtc_endpoint_contains_expected_transport(self) -> None: + channel = self.make_request( + "GET", f"{PATH_PREFIX}/rtc/transports", access_token=self._alice_tok + ) + self.assertEqual(200, channel.code, channel.json_body) + self.assert_dict({"rtc_transports": [RTC_ENDPOINT]}, channel.json_body) + + @override_config( + { + "experimental_features": {"msc4143_enabled": True}, + "matrix_rtc": {"transports": []}, + } + ) + def test_matrixrtc_endpoint_no_transports_configured(self) -> None: + channel = self.make_request( + "GET", f"{PATH_PREFIX}/rtc/transports", access_token=self._alice_tok + ) + self.assertEqual(200, channel.code, channel.json_body) + self.assert_dict({}, channel.json_body) + + @override_config( + { + "experimental_features": {"msc4143_enabled": True}, + "matrix_rtc": {"transports": [LIVEKIT_ENDPOINT]}, + } + ) + def test_matrixrtc_endpoint_livekit_transport(self) -> None: + channel = self.make_request( + "GET", f"{PATH_PREFIX}/rtc/transports", access_token=self._alice_tok + ) + self.assertEqual(200, channel.code, channel.json_body) + self.assert_dict({"rtc_transports": [LIVEKIT_ENDPOINT]}, channel.json_body)