mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-28 00:44:35 +00:00
MSC1763: Implement /_matrix/client/unstable/org.matrix.msc1763/retention/configuration (#19853)
Implements https://github.com/matrix-org/matrix-spec-proposals/pull/1763 Tracking issue https://github.com/element-hq/synapse/issues/19852 This only implements the MSC's configuration endpoint and does not add new configuration fields or options. The reason for including this is to allow clients to run global retention against locally stored events, and to be able to offer configuration UI. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
b38106d58b
commit
d3fc81974a
@@ -0,0 +1,3 @@
|
||||
The `GET /_matrix/client/unstable/org.matrix.msc1763/retention/configuration` endpoint is now provided when retention
|
||||
is enabled and `experimental_features.msc1763_enabled` is enabled, based on
|
||||
[MSC1763](https://github.com/matrix-org/matrix-spec-proposals/pull/1763).
|
||||
@@ -380,6 +380,9 @@ class ExperimentalConfig(Config):
|
||||
) -> None:
|
||||
experimental = config.get("experimental_features") or {}
|
||||
|
||||
# MSC1763 (retention policy configuration endpoint)
|
||||
self.msc1763_enabled: bool = experimental.get("msc1763_enabled", False)
|
||||
|
||||
# MSC3026 (busy presence state)
|
||||
self.msc3026_enabled: bool = experimental.get("msc3026_enabled", False)
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ from synapse.rest.client import (
|
||||
relations,
|
||||
rendezvous,
|
||||
reporting,
|
||||
retention,
|
||||
room,
|
||||
room_keys,
|
||||
room_upgrade_rest_servlet,
|
||||
@@ -107,6 +108,7 @@ CLIENT_SERVLET_FUNCTIONS: tuple[RegisterServletsFunc, ...] = (
|
||||
tags.register_servlets,
|
||||
account_data.register_servlets,
|
||||
reporting.register_servlets,
|
||||
retention.register_servlets,
|
||||
openid.register_servlets,
|
||||
notifications.register_servlets,
|
||||
devices.register_servlets,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2026 Element Creations 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:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from synapse.http.server import HttpServer
|
||||
from synapse.http.servlet import RestServlet
|
||||
from synapse.http.site import SynapseRequest
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
|
||||
class RetentionPolicyDict(TypedDict, total=False):
|
||||
min_lifetime: int
|
||||
max_lifetime: int
|
||||
|
||||
|
||||
class LifetimeBoundsDict(TypedDict, total=False):
|
||||
min: int
|
||||
max: int
|
||||
|
||||
|
||||
class RetentionLimitsDict(TypedDict, total=False):
|
||||
max_lifetime: LifetimeBoundsDict
|
||||
|
||||
|
||||
class RetentionConfigurationResponse(TypedDict):
|
||||
policies: dict[str, RetentionPolicyDict]
|
||||
limits: RetentionLimitsDict
|
||||
|
||||
|
||||
class RetentionConfigurationServlet(RestServlet):
|
||||
"""Implements MSC1763: /_matrix/client/unstable/org.matrix.msc1763/retention/configuration"""
|
||||
|
||||
PATTERNS = client_patterns(
|
||||
"/org.matrix.msc1763/retention/configuration$",
|
||||
releases=[],
|
||||
v1=False,
|
||||
unstable=True,
|
||||
)
|
||||
CATEGORY = "Client API requests"
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self._retention_config = hs.config.retention
|
||||
|
||||
async def on_GET(
|
||||
self, request: SynapseRequest
|
||||
) -> tuple[int, RetentionConfigurationResponse]:
|
||||
await self.auth.get_user_by_req(request)
|
||||
|
||||
default_policy: RetentionPolicyDict = {}
|
||||
if self._retention_config.retention_default_min_lifetime is not None:
|
||||
default_policy["min_lifetime"] = (
|
||||
self._retention_config.retention_default_min_lifetime
|
||||
)
|
||||
if self._retention_config.retention_default_max_lifetime is not None:
|
||||
default_policy["max_lifetime"] = (
|
||||
self._retention_config.retention_default_max_lifetime
|
||||
)
|
||||
|
||||
max_lifetime_limits: LifetimeBoundsDict = {}
|
||||
if self._retention_config.retention_allowed_lifetime_min is not None:
|
||||
max_lifetime_limits["min"] = (
|
||||
self._retention_config.retention_allowed_lifetime_min
|
||||
)
|
||||
if self._retention_config.retention_allowed_lifetime_max is not None:
|
||||
max_lifetime_limits["max"] = (
|
||||
self._retention_config.retention_allowed_lifetime_max
|
||||
)
|
||||
|
||||
limits: RetentionLimitsDict = {}
|
||||
if max_lifetime_limits:
|
||||
limits["max_lifetime"] = max_lifetime_limits
|
||||
|
||||
policies: dict[str, RetentionPolicyDict] = {}
|
||||
if default_policy:
|
||||
policies["*"] = default_policy
|
||||
|
||||
return HTTPStatus.OK, {"policies": policies, "limits": limits}
|
||||
|
||||
|
||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
if hs.config.retention.retention_enabled and hs.config.experimental.msc1763_enabled:
|
||||
RetentionConfigurationServlet(hs).register(http_server)
|
||||
@@ -2,6 +2,7 @@
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
# Copyright (C) 2026 Element Creations 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
|
||||
@@ -25,7 +26,7 @@ from twisted.internet.testing import MemoryReactor
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.events.utils import FilteredEvent
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.rest.client import login, retention, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict, create_requester
|
||||
from synapse.util.clock import Clock
|
||||
@@ -394,3 +395,122 @@ class RetentionNoDefaultPolicyTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
return channel.json_body
|
||||
|
||||
|
||||
RETENTION_CONFIGURATION_URL = (
|
||||
"/_matrix/client/unstable/org.matrix.msc1763/retention/configuration"
|
||||
)
|
||||
|
||||
|
||||
class RetentionConfigurationEndpointTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
retention.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.register_user("user", "password")
|
||||
self.token = self.login("user", "password")
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_disabled_returns_404_no_retention(self) -> None:
|
||||
"""The endpoint must 404 when retention is not enabled."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"default_policy": {
|
||||
"min_lifetime": one_day_ms,
|
||||
"max_lifetime": one_day_ms * 3,
|
||||
},
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 3,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": False},
|
||||
}
|
||||
)
|
||||
def test_disabled_returns_404_no_msc1763_enabled(self) -> None:
|
||||
"""The endpoint must 404 when retention is not enabled."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 404, channel.result)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"default_policy": {
|
||||
"min_lifetime": one_day_ms,
|
||||
"max_lifetime": one_day_ms * 3,
|
||||
},
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 3,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_full_config(self) -> None:
|
||||
"""Returns default policy and max_lifetime limits when fully configured."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertEqual(
|
||||
body["policies"]["*"],
|
||||
{"min_lifetime": one_day_ms, "max_lifetime": one_day_ms * 3},
|
||||
)
|
||||
self.assertEqual(
|
||||
body["limits"]["max_lifetime"],
|
||||
{"min": one_day_ms, "max": one_day_ms * 3},
|
||||
)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {"enabled": True},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_no_default_policy_no_limits(self) -> None:
|
||||
"""Returns empty policies and limits when nothing is configured."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertEqual(body["policies"], {})
|
||||
self.assertEqual(body["limits"], {})
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"retention": {
|
||||
"enabled": True,
|
||||
"allowed_lifetime_min": one_day_ms,
|
||||
"allowed_lifetime_max": one_day_ms * 7,
|
||||
},
|
||||
"experimental_features": {"msc1763_enabled": True},
|
||||
}
|
||||
)
|
||||
def test_limits_only(self) -> None:
|
||||
"""Returns limits but no default policy entry when only limits are set."""
|
||||
channel = self.make_request(
|
||||
"GET", RETENTION_CONFIGURATION_URL, access_token=self.token
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
body = channel.json_body
|
||||
self.assertNotIn("*", body["policies"])
|
||||
self.assertEqual(
|
||||
body["limits"]["max_lifetime"],
|
||||
{"min": one_day_ms, "max": one_day_ms * 7},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user