diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py
index 36e0ddab5c..3d067d29db 100644
--- a/synapse/config/spam_checker.py
+++ b/synapse/config/spam_checker.py
@@ -13,6 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from typing import Any, Dict, List, Tuple
+
+from synapse.config import ConfigError
from synapse.util.module_loader import load_module
from ._base import Config
@@ -22,16 +25,35 @@ class SpamCheckerConfig(Config):
section = "spamchecker"
def read_config(self, config, **kwargs):
- self.spam_checker = None
+ self.spam_checkers = [] # type: List[Tuple[Any, Dict]]
+
+ spam_checkers = config.get("spam_checker") or []
+ if isinstance(spam_checkers, dict):
+ # The spam_checker config option used to only support one
+ # spam checker, and thus was simply a dictionary with module
+ # and config keys. Support this old behaviour by checking
+ # to see if the option resolves to a dictionary
+ self.spam_checkers.append(load_module(spam_checkers))
+ elif isinstance(spam_checkers, list):
+ for spam_checker in spam_checkers:
+ if not isinstance(spam_checker, dict):
+ raise ConfigError("spam_checker syntax is incorrect")
- provider = config.get("spam_checker", None)
- if provider is not None:
- self.spam_checker = load_module(provider)
+ self.spam_checkers.append(load_module(spam_checker))
+ else:
+ raise ConfigError("spam_checker syntax is incorrect")
def generate_config_section(self, **kwargs):
return """\
- #spam_checker:
- # module: "my_custom_project.SuperSpamChecker"
- # config:
- # example_option: 'things'
+ # Spam checkers are third-party modules that can block specific actions
+ # of local users, such as creating rooms and registering undesirable
+ # usernames, as well as remote users by redacting incoming events.
+ #
+ spam_checker:
+ #- module: "my_custom_project.SuperSpamChecker"
+ # config:
+ # example_option: 'things'
+ #- module: "some_other_project.BadEventStopper"
+ # config:
+ # example_stop_events_from: ['@bad:example.com']
"""
diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
index a23b6b7b61..1ffc9525d1 100644
--- a/synapse/events/spamcheck.py
+++ b/synapse/events/spamcheck.py
@@ -15,7 +15,7 @@
# limitations under the License.
import inspect
-from typing import Dict
+from typing import Any, Dict, List
from synapse.spam_checker_api import SpamCheckerApi
@@ -26,24 +26,17 @@ if MYPY:
class SpamChecker(object):
def __init__(self, hs: "synapse.server.HomeServer"):
- self.spam_checker = None
+ self.spam_checkers = [] # type: List[Any]
- module = None
- config = None
- try:
- module, config = hs.config.spam_checker
- except Exception:
- pass
-
- if module is not None:
+ for module, config in hs.config.spam_checkers:
# Older spam checkers don't accept the `api` argument, so we
# try and detect support.
spam_args = inspect.getfullargspec(module)
if "api" in spam_args.args:
api = SpamCheckerApi(hs)
- self.spam_checker = module(config=config, api=api)
+ self.spam_checkers.append(module(config=config, api=api))
else:
- self.spam_checker = module(config=config)
+ self.spam_checkers.append(module(config=config))
def check_event_for_spam(self, event: "synapse.events.EventBase") -> bool:
"""Checks if a given event is considered "spammy" by this server.
@@ -58,10 +51,11 @@ class SpamChecker(object):
Returns:
True if the event is spammy.
"""
- if self.spam_checker is None:
- return False
+ for spam_checker in self.spam_checkers:
+ if spam_checker.check_event_for_spam(event):
+ return True
- return self.spam_checker.check_event_for_spam(event)
+ return False
def user_may_invite(
self, inviter_userid: str, invitee_userid: str, room_id: str
@@ -78,12 +72,14 @@ class SpamChecker(object):
Returns:
True if the user may send an invite, otherwise False
"""
- if self.spam_checker is None:
- return True
+ for spam_checker in self.spam_checkers:
+ if (
+ spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id)
+ is False
+ ):
+ return False
- return self.spam_checker.user_may_invite(
- inviter_userid, invitee_userid, room_id
- )
+ return True
def user_may_create_room(self, userid: str) -> bool:
"""Checks if a given user may create a room
@@ -96,10 +92,11 @@ class SpamChecker(object):
Returns:
True if the user may create a room, otherwise False
"""
- if self.spam_checker is None:
- return True
+ for spam_checker in self.spam_checkers:
+ if spam_checker.user_may_create_room(userid) is False:
+ return False
- return self.spam_checker.user_may_create_room(userid)
+ return True
def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool:
"""Checks if a given user may create a room alias
@@ -113,10 +110,11 @@ class SpamChecker(object):
Returns:
True if the user may create a room alias, otherwise False
"""
- if self.spam_checker is None:
- return True
+ for spam_checker in self.spam_checkers:
+ if spam_checker.user_may_create_room_alias(userid, room_alias) is False:
+ return False
- return self.spam_checker.user_may_create_room_alias(userid, room_alias)
+ return True
def user_may_publish_room(self, userid: str, room_id: str) -> bool:
"""Checks if a given user may publish a room to the directory
@@ -130,10 +128,11 @@ class SpamChecker(object):
Returns:
True if the user may publish the room, otherwise False
"""
- if self.spam_checker is None:
- return True
+ for spam_checker in self.spam_checkers:
+ if spam_checker.user_may_publish_room(userid, room_id) is False:
+ return False
- return self.spam_checker.user_may_publish_room(userid, room_id)
+ return True
def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
"""Checks if a user ID or display name are considered "spammy" by this server.
@@ -150,13 +149,14 @@ class SpamChecker(object):
Returns:
True if the user is spammy.
"""
- if self.spam_checker is None:
- return False
-
- # For backwards compatibility, if the method does not exist on the spam checker, fallback to not interfering.
- checker = getattr(self.spam_checker, "check_username_for_spam", None)
- if not checker:
- return False
- # Make a copy of the user profile object to ensure the spam checker
- # cannot modify it.
- return checker(user_profile.copy())
+ for spam_checker in self.spam_checkers:
+ # For backwards compatibility, only run if the method exists on the
+ # spam checker
+ checker = getattr(spam_checker, "check_username_for_spam", None)
+ if checker:
+ # Make a copy of the user profile object to ensure the spam checker
+ # cannot modify it.
+ if checker(user_profile.copy()):
+ return True
+
+ return False
|