Add logic for Mjolnir antispam module

This commit is contained in:
Travis Ralston 2019-10-25 20:59:19 -06:00
parent cc0ab174b3
commit 187a76a3e8
3 changed files with 228 additions and 10 deletions

View File

@ -1,24 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright 2019 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
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):
def __init__(self, config):
self._block_invites = config.get("block_invites", True)
self._block_messages = config.get("block_messages", False)
self._list_room_ids = config.get("ban_lists", [])
def __init__(self, config, hs):
self.block_invites = config.get("block_invites", True)
self.block_messages = config.get("block_messages", False)
self.list_room_ids = config.get("ban_lists", [])
self.rooms_to_lists = {} # type: Dict[str, BanList]
self.hs = hs
# Now we build the ban lists so we can match them
self.build_lists()
def build_lists(self):
for room_id in self.list_room_ids:
self.build_list(room_id)
def build_list(self, room_id):
logger.info("Rebuilding ban list for %s" % (room_id))
self.get_list_for_room(room_id).build()
def get_list_for_room(self, room_id):
if room_id not in self.rooms_to_lists:
self.rooms_to_lists[room_id] = BanList(hs=self.hs, room_id=room_id)
return self.rooms_to_lists[room_id]
def is_user_banned(self, user_id):
for room_id in self.rooms_to_lists:
ban_list = self.rooms_to_lists[room_id]
for rule in ban_list.user_rules:
if rule.matches(user_id):
return rule.action == RECOMMENDATION_BAN
return False
def is_server_banned(self, server_name):
for room_id in self.rooms_to_lists:
ban_list = self.rooms_to_lists[room_id]
for rule in ban_list.server_rules:
if rule.matches(server_name):
return rule.action == RECOMMENDATION_BAN
return False
# --- spam checker interface below here ---
def check_event_for_spam(self, event):
return False # not spam
room_id = event.get("room_id", "")
event_type = event.get("type", "")
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:
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
if not self.block_messages:
return False # not spam (we aren't blocking messages)
sender = UserID.from_string(event.get("sender", ""))
if self.is_user_banned(sender.to_string()):
return True
if self.is_server_banned(sender.domain):
return True
return False # not spam (as far as we're concerned)
def user_may_invite(self, inviter_user_id, invitee_user_id, room_id):
return True # allowed
if not self.block_invites:
return True # allowed (we aren't blocking invites)
sender = UserID.from_string(inviter_user_id)
if self.is_user_banned(sender.to_string()):
return False
if self.is_server_banned(sender.domain):
return False
return True # allowed (as far as we're concerned)
def user_may_create_room(self, user_id):
return True # allowed
return True # allowed
def user_may_create_room_alias(self, user_id, room_alias):
return True # allowed
return True # allowed
def user_may_publish_room(self, user_id, room_id):
return True # allowed
return True # allowed
@staticmethod
def parse_config(config):
return config # no parsing needed
return config # no parsing needed

View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Copyright 2019 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from .list_rule import ListRule, ALL_RULE_TYPES, USER_RULE_TYPES, SERVER_RULE_TYPES, ROOM_RULE_TYPES
from twisted.internet import defer
from synapse.storage.state import StateFilter
logger = logging.getLogger("synapse.contrib." + __name__)
class BanList(object):
def __init__(self, hs, room_id):
self.hs = hs
self.room_id = room_id
self.server_rules = []
self.user_rules = []
self.room_rules = []
self.build()
@defer.inlineCallbacks
def build(self, with_event=None):
events = yield self.get_relevant_state_events()
if with_event is not None:
events = [*events, with_event]
self.server_rules = []
self.user_rules = []
self.room_rules = []
for event in events:
event_type = event.get("type", "")
state_key = event.get("state_key", "")
content = event.get("content", {})
if state_key is None:
continue # Some message event got in here?
# Skip over events which are replaced by with_event. We do this
# to ensure that when we rebuild the list we're using updated rules.
if with_event is not None:
w_event_type = with_event.get("type", "")
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:
continue
entity = content.get("entity", None)
recommendation = content.get("recommendation", None)
reason = content.get("reason", None)
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)
if event_type in USER_RULE_TYPES:
self.user_rules.append(rule)
elif event_type in ROOM_RULE_TYPES:
self.room_rules.append(rule)
elif event_type in SERVER_RULE_TYPES:
self.server_rules.append(rule)
@defer.inlineCallbacks
def get_relevant_state_events(self):
store = self.hs.get_datastore()
ev_filter = StateFilter.from_types([(t, None) for t in ALL_RULE_TYPES])
state_ids = yield store.get_filtered_current_state_ids(
room_id=self.room_id, state_filter=ev_filter
)
state = yield store.get_events(state_ids.values())
return state.values()

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Copyright 2019 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.util import glob_to_regex
RECOMMENDATION_BAN = "m.ban"
RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]
RULE_USER = "m.room.rule.user"
RULE_ROOM = "m.room.rule.room"
RULE_SERVER = "m.room.rule.server"
USER_RULE_TYPES = [RULE_USER, "org.matrix.mjolnir.rule.user"]
ROOM_RULE_TYPES = [RULE_ROOM, "org.matrix.mjolnir.rule.room"]
SERVER_RULE_TYPES = [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
if rule in ROOM_RULE_TYPES:
return RULE_ROOM
if rule in SERVER_RULE_TYPES:
return RULE_SERVER
return None
class ListRule(object):
def __init__(self, entity, action, reason, kind):
self.entity = entity
self.regex = glob_to_regex(entity)
self.action = recommendation_to_stable(action)
self.reason = reason
self.kind = rule_type_to_stable(kind)
def matches(self, victim):
return self.regex.match(victim)