From 9c9bd0e02907412b5fa6b95844e9f53ac07b61fd Mon Sep 17 00:00:00 2001 From: David Teller Date: Wed, 9 Feb 2022 08:40:33 +0100 Subject: [PATCH] Let's port to Synapse module API (#128) --- README.md | 40 +++++++++++---------- mx-tester.yml | 12 +++++++ synapse_antispam/mjolnir/__init__.py | 1 + synapse_antispam/mjolnir/antispam.py | 50 +++++++++++++++++++++++++-- synapse_antispam/mjolnir/ban_list.py | 28 ++++++++++++--- synapse_antispam/mjolnir/list_rule.py | 3 ++ synapse_antispam/setup.py | 2 +- 7 files changed, 108 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6fa7faf..8ace2fb 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ set up: ## Synapse Module +**This requires Synapse 1.37.0 or higher** + Using the bot to manage your rooms is great, however if you want to use your ban lists (or someone else's) on your server to affect all of your users then a Synapse module is needed. Primarily meant to block invites from undesired homeservers/users, Mjolnir's @@ -99,25 +101,25 @@ pip install -e "git+https://github.com/matrix-org/mjolnir.git#egg=mjolnir&subdir Then add the following to your `homeserver.yaml`: ```yaml -spam_checker: - module: mjolnir.AntiSpam - config: - # Prevent servers/users in the ban lists from inviting users on this - # server to rooms. Default true. - block_invites: true - # Flag messages sent by servers/users in the ban lists as spam. Currently - # this means that spammy messages will appear as empty to users. Default - # false. - block_messages: false - # Remove users from the user directory search by filtering matrix IDs and - # display names by the entries in the user ban list. Default false. - block_usernames: false - # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir, - # this list cannot be room aliases or permalinks. This server is expected - # to already be joined to the room - Mjolnir will not automatically join - # these rooms. - ban_lists: - - "!roomid:example.org" +modules: + - module: mjolnir.Module + config: + # Prevent servers/users in the ban lists from inviting users on this + # server to rooms. Default true. + block_invites: true + # Flag messages sent by servers/users in the ban lists as spam. Currently + # this means that spammy messages will appear as empty to users. Default + # false. + block_messages: false + # Remove users from the user directory search by filtering matrix IDs and + # display names by the entries in the user ban list. Default false. + block_usernames: false + # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir, + # this list cannot be room aliases or permalinks. This server is expected + # to already be joined to the room - Mjolnir will not automatically join + # these rooms. + ban_lists: + - "!roomid:example.org" ``` *Note*: Although this is described as a "spam checker", it does much more than fight diff --git a/mx-tester.yml b/mx-tester.yml index 0a3372c..26ae0eb 100644 --- a/mx-tester.yml +++ b/mx-tester.yml @@ -1,4 +1,5 @@ name: mjolnir + up: before: # Launch the reverse proxy, listening for connections *only* on the local host. @@ -10,9 +11,20 @@ up: run: - yarn test:integration + down: finally: - docker stop mjolnir-test-reverse-proxy || true + +modules: + - name: mjolnir + build: + - cp -r synapse_antispam $MX_TEST_MODULE_DIR + config: + module: mjolnir.Module + config: {} + + homeserver: # Basic configuration. server_name: localhost:9999 diff --git a/synapse_antispam/mjolnir/__init__.py b/synapse_antispam/mjolnir/__init__.py index 76c4852..7da4bfb 100644 --- a/synapse_antispam/mjolnir/__init__.py +++ b/synapse_antispam/mjolnir/__init__.py @@ -1 +1,2 @@ from .antispam import AntiSpam +from .antispam import Module diff --git a/synapse_antispam/mjolnir/antispam.py b/synapse_antispam/mjolnir/antispam.py index df71fa9..7015e26 100644 --- a/synapse_antispam/mjolnir/antispam.py +++ b/synapse_antispam/mjolnir/antispam.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2019-2022 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,13 +14,21 @@ # limitations under the License. import logging +from typing import Dict, Union from .list_rule import ALL_RULE_TYPES, RECOMMENDATION_BAN from .ban_list import BanList from synapse.types import UserID logger = logging.getLogger("synapse.contrib." + __name__) + class AntiSpam(object): + """ + Implements the old synapse spam-checker API, for compatibility with older configurations. + + See https://github.com/matrix-org/synapse/blob/master/docs/spam_checker.md + """ + def __init__(self, config, api): self.block_invites = config.get("block_invites", True) self.block_messages = config.get("block_messages", False) @@ -77,7 +85,11 @@ class AntiSpam(object): state_key = event.get("state_key", None) # Rebuild the rules if there's an event for our ban lists - if state_key is not None and event_type in ALL_RULE_TYPES and room_id in self.list_room_ids: + if ( + state_key is not None + and event_type in ALL_RULE_TYPES + and room_id in self.list_room_ids + ): logger.info("Received ban list event - updating list") self.get_list_for_room(room_id).build(with_event=event) return False # Ban list updates aren't spam @@ -113,7 +125,9 @@ class AntiSpam(object): # Check whether the user ID or display name matches any of the banned # patterns. - return self.is_user_banned(user_profile["user_id"]) or self.is_user_banned(user_profile["display_name"]) + return self.is_user_banned(user_profile["user_id"]) or self.is_user_banned( + user_profile["display_name"] + ) def user_may_create_room(self, user_id): return True # allowed @@ -127,3 +141,33 @@ class AntiSpam(object): @staticmethod def parse_config(config): return config # no parsing needed + + +# New module API +class Module: + """ + Our main entry point. Implements the Synapse Module API. + """ + + def __init__(self, config, api): + self.antispam = AntiSpam(config, api) + self.antispam.api.register_spam_checker_callbacks( + check_event_for_spam=self.check_event_for_spam, + user_may_invite=self.user_may_invite, + check_username_for_spam=self.check_username_for_spam, + ) + + # Callbacks for `register_spam_checker_callbacks` + # Note that these are `async`, by opposition to the APIs in `AntiSpam`. + async def check_event_for_spam( + self, event: "synapse.events.EventBase" + ) -> Union[bool, str]: + return self.antispam.check_event_for_spam(event) + + async def user_may_invite( + self, inviter_user_id: str, invitee_user_id: str, room_id: str + ) -> bool: + return self.antispam.user_may_invite(inviter_user_id, invitee_user_id, room_id) + + async def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool: + return self.antispam.check_username_for_spam(user_profile) diff --git a/synapse_antispam/mjolnir/ban_list.py b/synapse_antispam/mjolnir/ban_list.py index 57b7ad0..09e8a5d 100644 --- a/synapse_antispam/mjolnir/ban_list.py +++ b/synapse_antispam/mjolnir/ban_list.py @@ -14,12 +14,19 @@ # limitations under the License. import logging -from .list_rule import ListRule, ALL_RULE_TYPES, USER_RULE_TYPES, SERVER_RULE_TYPES, ROOM_RULE_TYPES +from .list_rule import ( + ListRule, + ALL_RULE_TYPES, + USER_RULE_TYPES, + SERVER_RULE_TYPES, + ROOM_RULE_TYPES, +) from twisted.internet import defer from synapse.metrics.background_process_metrics import run_as_background_process logger = logging.getLogger("synapse.contrib." + __name__) + class BanList(object): def __init__(self, api, room_id): self.api = api @@ -52,7 +59,11 @@ class BanList(object): w_state_key = with_event.get("state_key", "") w_event_id = with_event.event_id event_id = event.event_id - if w_event_type == event_type and w_state_key == state_key and w_event_id != event_id: + if ( + w_event_type == event_type + and w_state_key == state_key + and w_event_id != event_id + ): continue entity = content.get("entity", None) @@ -61,8 +72,13 @@ class BanList(object): if entity is None or recommendation is None or reason is None: continue # invalid event - logger.info("Adding rule %s/%s with action %s" % (event_type, entity, recommendation)) - rule = ListRule(entity=entity, action=recommendation, reason=reason, kind=event_type) + logger.info( + "Adding rule %s/%s with action %s" + % (event_type, entity, recommendation) + ) + rule = ListRule( + entity=entity, action=recommendation, reason=reason, kind=event_type + ) if event_type in USER_RULE_TYPES: self.user_rules.append(rule) elif event_type in ROOM_RULE_TYPES: @@ -73,4 +89,6 @@ class BanList(object): run_as_background_process("mjolnir_build_ban_list", run, with_event) def get_relevant_state_events(self): - return self.api.get_state_events_in_room(self.room_id, [(t, None) for t in ALL_RULE_TYPES]) + return self.api.get_state_events_in_room( + self.room_id, [(t, None) for t in ALL_RULE_TYPES] + ) diff --git a/synapse_antispam/mjolnir/list_rule.py b/synapse_antispam/mjolnir/list_rule.py index d914946..b21d1e3 100644 --- a/synapse_antispam/mjolnir/list_rule.py +++ b/synapse_antispam/mjolnir/list_rule.py @@ -26,11 +26,13 @@ ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room" SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"] ALL_RULE_TYPES = [*USER_RULE_TYPES, *ROOM_RULE_TYPES, *SERVER_RULE_TYPES] + def recommendation_to_stable(recommendation): if recommendation in RECOMMENDATION_BAN_TYPES: return RECOMMENDATION_BAN return None + def rule_type_to_stable(rule): if rule in USER_RULE_TYPES: return RULE_USER @@ -40,6 +42,7 @@ def rule_type_to_stable(rule): return RULE_SERVER return None + class ListRule(object): def __init__(self, entity, action, reason, kind): self.entity = entity diff --git a/synapse_antispam/setup.py b/synapse_antispam/setup.py index 53bcf4d..ad0b4f1 100644 --- a/synapse_antispam/setup.py +++ b/synapse_antispam/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="mjolnir", - version="0.0.1", + version="0.1.0", packages=find_packages(), description="Mjolnir Antispam", include_package_data=True,