summary refs log tree commit diff
path: root/packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch
diff options
context:
space:
mode:
Diffstat (limited to 'packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch')
-rw-r--r--packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch666
1 files changed, 0 insertions, 666 deletions
diff --git a/packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch b/packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch
deleted file mode 100644

index 528c970..0000000 --- a/packages/overlays/matrix-synapse/patches/0024-Policy-server-part-1-Actually-call-the-policy-server.patch +++ /dev/null
@@ -1,666 +0,0 @@ -From b7d48419476f70e54dc24ecd986562ba22be52ec Mon Sep 17 00:00:00 2001 -From: Travis Ralston <travisr@element.io> -Date: Wed, 21 May 2025 16:09:09 -0600 -Subject: [PATCH 24/34] Policy server part 1: Actually call the policy server - (#18387) - -Roughly reviewable commit-by-commit. - -This is the first part of adding policy server support to Synapse. Other -parts (unordered), which may or may not be bundled into fewer PRs, -include: - -* Implementation of a bulk API -* Supporting a moderation server config (the `fallback_*` options of -https://github.com/element-hq/policyserv_spam_checker ) -* Adding an "early event hook" for appservices to receive federation -transactions *before* events are processed formally -* Performance and stability improvements - -### Pull Request Checklist - -<!-- Please read -https://element-hq.github.io/synapse/latest/development/contributing_guide.html -before submitting your pull request --> - -* [x] Pull request is based on the develop branch -* [x] Pull request includes a [changelog -file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). -The entry should: -- Be a short description of your change which makes sense to users. -"Fixed a bug that prevented receiving messages from other servers." -instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - - Use markdown where necessary, mostly for `code blocks`. - - End with either a period (.) or an exclamation mark (!). - - Start with a capital letter. -- Feel free to credit yourself, by adding a sentence "Contributed by -@github_username." or "Contributed by [Your Name]." to the end of the -entry. -* [x] [Code -style](https://element-hq.github.io/synapse/latest/code_style.html) is -correct -(run the -[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) - ---------- - -Co-authored-by: turt2live <1190097+turt2live@users.noreply.github.com> -Co-authored-by: Devon Hudson <devon.dmytro@gmail.com> ---- - changelog.d/18387.feature | 1 + - synapse/federation/federation_base.py | 34 ++++ - synapse/federation/federation_client.py | 57 ++++++ - synapse/federation/transport/client.py | 27 +++ - synapse/handlers/message.py | 15 +- - synapse/handlers/room_policy.py | 89 ++++++++++ - synapse/server.py | 5 + - synapse/types/handlers/policy_server.py | 16 ++ - tests/handlers/test_room_policy.py | 226 ++++++++++++++++++++++++ - 9 files changed, 469 insertions(+), 1 deletion(-) - create mode 100644 changelog.d/18387.feature - create mode 100644 synapse/handlers/room_policy.py - create mode 100644 synapse/types/handlers/policy_server.py - create mode 100644 tests/handlers/test_room_policy.py - -diff --git a/changelog.d/18387.feature b/changelog.d/18387.feature -new file mode 100644 -index 0000000000..2d9ff2cea2 ---- /dev/null -+++ b/changelog.d/18387.feature -@@ -0,0 +1 @@ -+Add support for calling Policy Servers ([MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284)) to mark events as spam. -\ No newline at end of file -diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py -index 3796bff5e7..45593430e8 100644 ---- a/synapse/federation/federation_base.py -+++ b/synapse/federation/federation_base.py -@@ -30,6 +30,7 @@ from synapse.crypto.keyring import Keyring - from synapse.events import EventBase, make_event_from_dict - from synapse.events.utils import prune_event, validate_canonicaljson - from synapse.federation.units import filter_pdus_for_valid_depth -+from synapse.handlers.room_policy import RoomPolicyHandler - from synapse.http.servlet import assert_params_in_dict - from synapse.logging.opentracing import log_kv, trace - from synapse.types import JsonDict, get_domain_from_id -@@ -64,6 +65,24 @@ class FederationBase: - self._clock = hs.get_clock() - self._storage_controllers = hs.get_storage_controllers() - -+ # We need to define this lazily otherwise we get a cyclic dependency. -+ # self._policy_handler = hs.get_room_policy_handler() -+ self._policy_handler: Optional[RoomPolicyHandler] = None -+ -+ def _lazily_get_policy_handler(self) -> RoomPolicyHandler: -+ """Lazily get the room policy handler. -+ -+ This is required to avoid an import cycle: RoomPolicyHandler requires a -+ FederationClient, which requires a FederationBase, which requires a -+ RoomPolicyHandler. -+ -+ Returns: -+ RoomPolicyHandler: The room policy handler. -+ """ -+ if self._policy_handler is None: -+ self._policy_handler = self.hs.get_room_policy_handler() -+ return self._policy_handler -+ - @trace - async def _check_sigs_and_hash( - self, -@@ -80,6 +99,10 @@ class FederationBase: - Also runs the event through the spam checker; if it fails, redacts the event - and flags it as soft-failed. - -+ Also checks that the event is allowed by the policy server, if the room uses -+ a policy server. If the event is not allowed, the event is flagged as -+ soft-failed but not redacted. -+ - Args: - room_version: The room version of the PDU - pdu: the event to be checked -@@ -145,6 +168,17 @@ class FederationBase: - ) - return redacted_event - -+ policy_allowed = await self._lazily_get_policy_handler().is_event_allowed(pdu) -+ if not policy_allowed: -+ logger.warning( -+ "Event not allowed by policy server, soft-failing %s", pdu.event_id -+ ) -+ pdu.internal_metadata.soft_failed = True -+ # Note: we don't redact the event so admins can inspect the event after the -+ # fact. Other processes may redact the event, but that won't be applied to -+ # the database copy of the event until the server's config requires it. -+ return pdu -+ - spam_check = await self._spam_checker_module_callbacks.check_event_for_spam(pdu) - - if spam_check != self._spam_checker_module_callbacks.NOT_SPAM: -diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py -index 9fc5b70e9a..7c485aa7e0 100644 ---- a/synapse/federation/federation_client.py -+++ b/synapse/federation/federation_client.py -@@ -75,6 +75,7 @@ from synapse.http.client import is_unknown_endpoint - from synapse.http.types import QueryParams - from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace - from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id -+from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM - from synapse.util.async_helpers import concurrently_execute - from synapse.util.caches.expiringcache import ExpiringCache - from synapse.util.retryutils import NotRetryingDestination -@@ -421,6 +422,62 @@ class FederationClient(FederationBase): - - return None - -+ @trace -+ @tag_args -+ async def get_pdu_policy_recommendation( -+ self, destination: str, pdu: EventBase, timeout: Optional[int] = None -+ ) -> str: -+ """Requests that the destination server (typically a policy server) -+ check the event and return its recommendation on how to handle the -+ event. -+ -+ If the policy server could not be contacted or the policy server -+ returned an unknown recommendation, this returns an OK recommendation. -+ This type fixing behaviour is done because the typical caller will be -+ in a critical call path and would generally interpret a `None` or similar -+ response as "weird value; don't care; move on without taking action". We -+ just frontload that logic here. -+ -+ -+ Args: -+ destination: The remote homeserver to ask (a policy server) -+ pdu: The event to check -+ timeout: How long to try (in ms) the destination for before -+ giving up. None indicates no timeout. -+ -+ Returns: -+ The policy recommendation, or RECOMMENDATION_OK if the policy server was -+ uncontactable or returned an unknown recommendation. -+ """ -+ -+ logger.debug( -+ "get_pdu_policy_recommendation for event_id=%s from %s", -+ pdu.event_id, -+ destination, -+ ) -+ -+ try: -+ res = await self.transport_layer.get_policy_recommendation_for_pdu( -+ destination, pdu, timeout=timeout -+ ) -+ recommendation = res.get("recommendation") -+ if not isinstance(recommendation, str): -+ raise InvalidResponseError("recommendation is not a string") -+ if recommendation not in (RECOMMENDATION_OK, RECOMMENDATION_SPAM): -+ logger.warning( -+ "get_pdu_policy_recommendation: unknown recommendation: %s", -+ recommendation, -+ ) -+ return RECOMMENDATION_OK -+ return recommendation -+ except Exception as e: -+ logger.warning( -+ "get_pdu_policy_recommendation: server %s responded with error, assuming OK recommendation: %s", -+ destination, -+ e, -+ ) -+ return RECOMMENDATION_OK -+ - @trace - @tag_args - async def get_pdu( -diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py -index 206e91ed14..62bf96ce91 100644 ---- a/synapse/federation/transport/client.py -+++ b/synapse/federation/transport/client.py -@@ -143,6 +143,33 @@ class TransportLayerClient: - destination, path=path, timeout=timeout, try_trailing_slash_on_400=True - ) - -+ async def get_policy_recommendation_for_pdu( -+ self, destination: str, event: EventBase, timeout: Optional[int] = None -+ ) -> JsonDict: -+ """Requests the policy recommendation for the given pdu from the given policy server. -+ -+ Args: -+ destination: The host name of the remote homeserver checking the event. -+ event: The event to check. -+ timeout: How long to try (in ms) the destination for before giving up. -+ None indicates no timeout. -+ -+ Returns: -+ The full recommendation object from the remote server. -+ """ -+ logger.debug( -+ "get_policy_recommendation_for_pdu dest=%s, event_id=%s", -+ destination, -+ event.event_id, -+ ) -+ return await self.client.post_json( -+ destination=destination, -+ path=f"/_matrix/policy/unstable/org.matrix.msc4284/event/{event.event_id}/check", -+ data=event.get_pdu_json(), -+ ignore_backoff=True, -+ timeout=timeout, -+ ) -+ - async def backfill( - self, destination: str, room_id: str, event_tuples: Collection[str], limit: int - ) -> Optional[Union[JsonDict, list]]: -diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py -index ff6eb5a514..cb6de02309 100644 ---- a/synapse/handlers/message.py -+++ b/synapse/handlers/message.py -@@ -495,6 +495,7 @@ class EventCreationHandler: - self._instance_name = hs.get_instance_name() - self._notifier = hs.get_notifier() - self._worker_lock_handler = hs.get_worker_locks_handler() -+ self._policy_handler = hs.get_room_policy_handler() - - self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state - -@@ -1108,6 +1109,18 @@ class EventCreationHandler: - event.sender, - ) - -+ policy_allowed = await self._policy_handler.is_event_allowed(event) -+ if not policy_allowed: -+ logger.warning( -+ "Event not allowed by policy server, rejecting %s", -+ event.event_id, -+ ) -+ raise SynapseError( -+ 403, -+ "This message has been rejected as probable spam", -+ Codes.FORBIDDEN, -+ ) -+ - spam_check_result = ( - await self._spam_checker_module_callbacks.check_event_for_spam( - event -@@ -1119,7 +1132,7 @@ class EventCreationHandler: - [code, dict] = spam_check_result - raise SynapseError( - 403, -- "This message had been rejected as probable spam", -+ "This message has been rejected as probable spam", - code, - dict, - ) -diff --git a/synapse/handlers/room_policy.py b/synapse/handlers/room_policy.py -new file mode 100644 -index 0000000000..dcfebb128c ---- /dev/null -+++ b/synapse/handlers/room_policy.py -@@ -0,0 +1,89 @@ -+# -+# This file is licensed under the Affero General Public License (AGPL) version 3. -+# -+# Copyright 2016-2021 The Matrix.org Foundation C.I.C. -+# Copyright (C) 2023 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: -+# <https://www.gnu.org/licenses/agpl-3.0.html>. -+# -+# -+ -+import logging -+from typing import TYPE_CHECKING -+ -+from synapse.events import EventBase -+from synapse.types.handlers.policy_server import RECOMMENDATION_OK -+from synapse.util.stringutils import parse_and_validate_server_name -+ -+if TYPE_CHECKING: -+ from synapse.server import HomeServer -+ -+logger = logging.getLogger(__name__) -+ -+ -+class RoomPolicyHandler: -+ def __init__(self, hs: "HomeServer"): -+ self._hs = hs -+ self._store = hs.get_datastores().main -+ self._storage_controllers = hs.get_storage_controllers() -+ self._event_auth_handler = hs.get_event_auth_handler() -+ self._federation_client = hs.get_federation_client() -+ -+ async def is_event_allowed(self, event: EventBase) -> bool: -+ """Check if the given event is allowed in the room by the policy server. -+ -+ Note: This will *always* return True if the room's policy server is Synapse -+ itself. This is because Synapse can't be a policy server (currently). -+ -+ If no policy server is configured in the room, this returns True. Similarly, if -+ the policy server is invalid in any way (not joined, not a server, etc), this -+ returns True. -+ -+ If a valid and contactable policy server is configured in the room, this returns -+ True if that server suggests the event is not spammy, and False otherwise. -+ -+ Args: -+ event: The event to check. This should be a fully-formed PDU. -+ -+ Returns: -+ bool: True if the event is allowed in the room, False otherwise. -+ """ -+ policy_event = await self._storage_controllers.state.get_current_state_event( -+ event.room_id, "org.matrix.msc4284.policy", "" -+ ) -+ if not policy_event: -+ return True # no policy server == default allow -+ -+ policy_server = policy_event.content.get("via", "") -+ if policy_server is None or not isinstance(policy_server, str): -+ return True # no policy server == default allow -+ -+ if policy_server == self._hs.hostname: -+ return True # Synapse itself can't be a policy server (currently) -+ -+ try: -+ parse_and_validate_server_name(policy_server) -+ except ValueError: -+ return True # invalid policy server == default allow -+ -+ is_in_room = await self._event_auth_handler.is_host_in_room( -+ event.room_id, policy_server -+ ) -+ if not is_in_room: -+ return True # policy server not in room == default allow -+ -+ # At this point, the server appears valid and is in the room, so ask it to check -+ # the event. -+ recommendation = await self._federation_client.get_pdu_policy_recommendation( -+ policy_server, event -+ ) -+ if recommendation != RECOMMENDATION_OK: -+ return False -+ -+ return True # default allow -diff --git a/synapse/server.py b/synapse/server.py -index bd2faa61b9..2add4d4e6e 100644 ---- a/synapse/server.py -+++ b/synapse/server.py -@@ -107,6 +107,7 @@ from synapse.handlers.room_member import ( - RoomMemberMasterHandler, - ) - from synapse.handlers.room_member_worker import RoomMemberWorkerHandler -+from synapse.handlers.room_policy import RoomPolicyHandler - from synapse.handlers.room_summary import RoomSummaryHandler - from synapse.handlers.search import SearchHandler - from synapse.handlers.send_email import SendEmailHandler -@@ -807,6 +808,10 @@ class HomeServer(metaclass=abc.ABCMeta): - - return OidcHandler(self) - -+ @cache_in_self -+ def get_room_policy_handler(self) -> RoomPolicyHandler: -+ return RoomPolicyHandler(self) -+ - @cache_in_self - def get_event_client_serializer(self) -> EventClientSerializer: - return EventClientSerializer(self) -diff --git a/synapse/types/handlers/policy_server.py b/synapse/types/handlers/policy_server.py -new file mode 100644 -index 0000000000..bfc09dabf4 ---- /dev/null -+++ b/synapse/types/handlers/policy_server.py -@@ -0,0 +1,16 @@ -+# -+# 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: -+# <https://www.gnu.org/licenses/agpl-3.0.html>. -+# -+ -+RECOMMENDATION_OK = "ok" -+RECOMMENDATION_SPAM = "spam" -diff --git a/tests/handlers/test_room_policy.py b/tests/handlers/test_room_policy.py -new file mode 100644 -index 0000000000..26642c18ea ---- /dev/null -+++ b/tests/handlers/test_room_policy.py -@@ -0,0 +1,226 @@ -+# -+# 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: -+# <https://www.gnu.org/licenses/agpl-3.0.html>. -+# -+# -+from typing import Optional -+from unittest import mock -+ -+from twisted.test.proto_helpers import MemoryReactor -+ -+from synapse.events import EventBase, make_event_from_dict -+from synapse.rest import admin -+from synapse.rest.client import login, room -+from synapse.server import HomeServer -+from synapse.types import JsonDict, UserID -+from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM -+from synapse.util import Clock -+ -+from tests import unittest -+from tests.test_utils import event_injection -+ -+ -+class RoomPolicyTestCase(unittest.FederatingHomeserverTestCase): -+ """Tests room policy handler.""" -+ -+ servlets = [ -+ admin.register_servlets, -+ login.register_servlets, -+ room.register_servlets, -+ ] -+ -+ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: -+ # mock out the federation transport client -+ self.mock_federation_transport_client = mock.Mock( -+ spec=["get_policy_recommendation_for_pdu"] -+ ) -+ self.mock_federation_transport_client.get_policy_recommendation_for_pdu = ( -+ mock.AsyncMock() -+ ) -+ return super().setup_test_homeserver( -+ federation_transport_client=self.mock_federation_transport_client -+ ) -+ -+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: -+ self.hs = hs -+ self.handler = hs.get_room_policy_handler() -+ main_store = self.hs.get_datastores().main -+ -+ # Create a room -+ self.creator = self.register_user("creator", "test1234") -+ self.creator_token = self.login("creator", "test1234") -+ self.room_id = self.helper.create_room_as( -+ room_creator=self.creator, tok=self.creator_token -+ ) -+ room_version = self.get_success(main_store.get_room_version(self.room_id)) -+ -+ # Create some sample events -+ self.spammy_event = make_event_from_dict( -+ room_version=room_version, -+ internal_metadata_dict={}, -+ event_dict={ -+ "room_id": self.room_id, -+ "type": "m.room.message", -+ "sender": "@spammy:example.org", -+ "content": { -+ "msgtype": "m.text", -+ "body": "This is a spammy event.", -+ }, -+ }, -+ ) -+ self.not_spammy_event = make_event_from_dict( -+ room_version=room_version, -+ internal_metadata_dict={}, -+ event_dict={ -+ "room_id": self.room_id, -+ "type": "m.room.message", -+ "sender": "@not_spammy:example.org", -+ "content": { -+ "msgtype": "m.text", -+ "body": "This is a NOT spammy event.", -+ }, -+ }, -+ ) -+ -+ # Prepare the policy server mock to decide spam vs not spam on those events -+ self.call_count = 0 -+ -+ async def get_policy_recommendation_for_pdu( -+ destination: str, -+ pdu: EventBase, -+ timeout: Optional[int] = None, -+ ) -> JsonDict: -+ self.call_count += 1 -+ self.assertEqual(destination, self.OTHER_SERVER_NAME) -+ if pdu.event_id == self.spammy_event.event_id: -+ return {"recommendation": RECOMMENDATION_SPAM} -+ elif pdu.event_id == self.not_spammy_event.event_id: -+ return {"recommendation": RECOMMENDATION_OK} -+ else: -+ self.fail("Unexpected event ID") -+ -+ self.mock_federation_transport_client.get_policy_recommendation_for_pdu.side_effect = get_policy_recommendation_for_pdu -+ -+ def _add_policy_server_to_room(self) -> None: -+ # Inject a member event into the room -+ policy_user_id = f"@policy:{self.OTHER_SERVER_NAME}" -+ self.get_success( -+ event_injection.inject_member_event( -+ self.hs, self.room_id, policy_user_id, "join" -+ ) -+ ) -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ "via": self.OTHER_SERVER_NAME, -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ def test_no_policy_event_set(self) -> None: -+ # We don't need to modify the room state at all - we're testing the default -+ # case where a room doesn't use a policy server. -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_empty_policy_event_set(self) -> None: -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ # empty content (no `via`) -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_nonstring_policy_event_set(self) -> None: -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ "via": 42, # should be a server name -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_self_policy_event_set(self) -> None: -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ # We ignore events when the policy server is ourselves (for now?) -+ "via": (UserID.from_string(self.creator)).domain, -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_invalid_server_policy_event_set(self) -> None: -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ "via": "|this| is *not* a (valid) server name.com", -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_not_in_room_policy_event_set(self) -> None: -+ self.helper.send_state( -+ self.room_id, -+ "org.matrix.msc4284.policy", -+ { -+ "via": f"x.{self.OTHER_SERVER_NAME}", -+ }, -+ tok=self.creator_token, -+ state_key="", -+ ) -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 0) -+ -+ def test_spammy_event_is_spam(self) -> None: -+ self._add_policy_server_to_room() -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.spammy_event)) -+ self.assertEqual(ok, False) -+ self.assertEqual(self.call_count, 1) -+ -+ def test_not_spammy_event_is_not_spam(self) -> None: -+ self._add_policy_server_to_room() -+ -+ ok = self.get_success(self.handler.is_event_allowed(self.not_spammy_event)) -+ self.assertEqual(ok, True) -+ self.assertEqual(self.call_count, 1) --- -2.49.0 -