diff --git a/base-config.yaml b/base-config.yaml index 95ebda6..f538c17 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -82,3 +82,10 @@ censor_files: true censor_wordlist: - 'effword' - 'essword' + +# list of banlists that should be subscribed to, such as #community-moderation-effort-bl:neko.dev +# when users join any room managed by this bot, they are compared against these existing banlists +# if found, they will immediately be banned. +# your bot MUST be in the banlist room already! +banlists: + - '#community-moderation-effort-bl:neko.dev' diff --git a/community/bot.py b/community/bot.py index 5cd9801..65c141a 100644 --- a/community/bot.py +++ b/community/bot.py @@ -4,6 +4,7 @@ from typing import Awaitable, Type, Optional, Tuple import json import time import re +import fnmatch from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType, @@ -38,6 +39,7 @@ class Config(BaseProxyConfig): helper.copy("censor") helper.copy("censor_wordlist") helper.copy("censor_files") + helper.copy("banlists") class CommunityBot(Plugin): @@ -126,7 +128,7 @@ class CommunityBot(Plugin): for w in self.config['censor_wordlist']: try: if bool(re.search(w, msg.content.body, re.IGNORECASE)): - self.log.debug(f"DEBUG message flagged for censorship") + #self.log.debug(f"DEBUG message flagged for censorship") return True else: pass @@ -135,21 +137,105 @@ class CommunityBot(Plugin): def censor_room(self, msg): if isinstance(self.config['censor'], bool): - self.log.debug(f"DEBUG message will be redacted because censoring is enabled") + #self.log.debug(f"DEBUG message will be redacted because censoring is enabled") return self.config['censor'] elif isinstance(self.config['censor'], list): if msg.room_id in self.config['censor']: - self.log.debug(f"DEBUG message will be redacted because censoring is enabled for THIS room") + #self.log.debug(f"DEBUG message will be redacted because censoring is enabled for THIS room") return True else: return False + async def check_if_banned(self, userid): + # fetch banlist data + is_banned = False + myrooms = await self.client.get_joined_rooms() + for l in self.config['banlists']: + #self.log.debug(f"DEBUG getting banlist {l}") + if l.startswith('#'): + try: + l_id = await self.client.resolve_room_alias(l) + list_id = l_id["room_id"] + #self.log.debug(f"DEBUG banlist id resolves to: {list_id}") + except: + evt.reply("i don't recognize that list, sorry") + return + else: + list_id = l + + if list_id not in myrooms: + self.log.error(f"Bot must be in {l} before attempting to use it as a banlist.") + pass + + #self.log.debug(f"DEBUG looking up state in {list_id}") + list_state = await self.client.get_state(list_id) + #self.log.debug(f"DEBUG state found: {list_state}") + try: + user_policies = list(filter(lambda p : p.type.t=='m.policy.rule.user', list_state)) + #self.log.debug(f"DEBUG user policies found: {user_policies}") + except Exception as e: + self.log.error(e) + + for rule in user_policies: + #self.log.debug(f"Checking match of user {userid} in banlist {l} for {rule['content']}") + try: + if bool(fnmatch.fnmatch(userid, rule["content"]["entity"])) and \ + bool(re.search('ban$', rule["content"]["recommendation"])): + #self.log.debug(f"DEBUG user {userid} matches ban rule {rule['content']['entity']}!") + return True + else: + pass + except Exception as e: + self.log.error(f"Found something funny in the banlist {l} for {rule['content']}: {e}") + pass + # if we haven't exited by now, we must not be banned! + return is_banned + + async def ban_this_user(self, user): + #self.log.debug(f"DEBUG getting list of rooms") + roomlist = await self.get_space_roomlist() + # don't forget to kick from the space itself + roomlist.append(self.config["parent_room"]) + #self.log.debug(f"DEBUG list of rooms acquired") + ban_event_map = {'ban_list':{}, 'error_list':{}} + + ban_event_map['ban_list'][user] = [] + #self.log.debug(f"DEBUG banning {user} from rooms...") + for room in roomlist: + try: + roomname = None + roomnamestate = await self.client.get_state_event(room, 'm.room.name') + roomname = roomnamestate['name'] + + # ban user even if they're not in the room! + #await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) + await self.client.ban_user(room, user, reason='banned') + if roomname: + ban_event_map['ban_list'][user].append(roomname) + else: + ban_event_map['ban_list'][user].append(room) + time.sleep(0.5) + except MNotFound: + pass + except Exception as e: + self.log.warning(e) + ban_event_map['error_list'][user] = [] + ban_event_map['error_list'][user].append(roomname or room) + + return ban_event_map + @event.on(InternalEventType.JOIN) async def newjoin(self, evt:StateEvent) -> None: if evt.source & SyncStream.STATE: return else: + on_banlist = await self.check_if_banned(evt.sender) + if on_banlist: + #self.log.debug(f"DEBUG user is on banlist!") + # ban this account in managed rooms, don't bother with anything else + await self.ban_this_user(evt.sender) + return # passive sync of tracking db if evt.room_id == self.config['parent_room']: await self.do_sync() @@ -213,6 +299,12 @@ class CommunityBot(Plugin): async def community(self) -> None: pass + @community.subcommand("bancheck", help="check subscribed banlists for a user's mxid") + @command.argument("mxid", "full matrix ID", required=True) + async def check_banlists(self, evt: MessageEvent, mxid: UserID) -> None: + ban_status = await self.check_if_banned(mxid) + await evt.reply(f"user on banlist: {ban_status}") + @community.subcommand("sync", help="update the activity tracker with the current space members \ in case they are missing") @@ -388,37 +480,12 @@ class CommunityBot(Plugin): if evt.sender in self.config["admins"]: user = mxid msg = await evt.respond("starting the ban...") - roomlist = await self.get_space_roomlist() - # don't forget to kick from the space itself - roomlist.append(self.config["parent_room"]) - ban_list = {} - error_list = {} - - ban_list[user] = [] - for room in roomlist: - try: - roomname = None - roomnamestate = await self.client.get_state_event(room, 'm.room.name') - roomname = roomnamestate['name'] - - # ban user even if they're not in the room! - #await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) - await self.client.ban_user(room, user, reason='banned') - if roomname: - ban_list[user].append(roomname) - else: - ban_list[user].append(room) - time.sleep(0.5) - except MNotFound: - pass - except Exception as e: - self.log.warning(e) - error_list[user] = [] - error_list[user].append(roomname or room) + results_map = await self.ban_this_user(user) results = "the following users were kicked and banned:
{ban_list}
{error_list}
{error_list}