Include redaction logic:

- redact command to remove messages from a user in a room
- option to redact messages from a user in all space rooms when banned
This commit is contained in:
William Kray
2025-03-31 15:25:00 -07:00
parent 6001f5bf1a
commit 034446f28c
4 changed files with 123 additions and 7 deletions
+3
View File
@@ -112,3 +112,6 @@ banlists:
# an account may join your rooms, THEN get added to the banlist, and you will have to manually # an account may join your rooms, THEN get added to the banlist, and you will have to manually
# ban them from your rooms. # ban them from your rooms.
proactive_banning: true proactive_banning: true
# should we redact messages when a user is banned?
redact_on_ban: true
+109 -5
View File
@@ -5,11 +5,12 @@ import json
import time import time
import re import re
import fnmatch import fnmatch
import asyncio
from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream
from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType, from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType,
MediaMessageEventContent, ReactionEvent, RedactionEvent, RoomID, MediaMessageEventContent, ReactionEvent, RedactionEvent, RoomID,
RoomAlias, PowerLevelStateEventContent, MessageType) RoomAlias, PowerLevelStateEventContent, MessageType, PaginationDirection)
from mautrix.errors import MNotFound from mautrix.errors import MNotFound
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent from maubot import Plugin, MessageEvent
@@ -46,15 +47,40 @@ class Config(BaseProxyConfig):
helper.copy("censor_files") helper.copy("censor_files")
helper.copy("banlists") helper.copy("banlists")
helper.copy("proactive_banning") helper.copy("proactive_banning")
helper.copy("redact_on_ban")
class CommunityBot(Plugin): class CommunityBot(Plugin):
_redaction_tasks: asyncio.Task = None
async def start(self) -> None: async def start(self) -> None:
await super().start() await super().start()
self.config.load_and_update() self.config.load_and_update()
self.client.add_dispatcher(MembershipEventDispatcher) self.client.add_dispatcher(MembershipEventDispatcher)
# Start background redaction task
self._redaction_tasks = asyncio.create_task(self._redaction_loop())
async def stop(self) -> None:
if self._redaction_tasks:
self._redaction_tasks.cancel()
await super().stop()
async def _redaction_loop(self) -> None:
while True:
try:
# Get all rooms with pending redactions
rooms = await self.database.fetch(
"SELECT DISTINCT room_id FROM redaction_tasks"
)
for room in rooms:
await self.redact_messages(room['room_id'])
await asyncio.sleep(60) # Run every minute
except asyncio.CancelledError:
break
except Exception as e:
self.log.error(f"Error in redaction loop: {e}")
await asyncio.sleep(60) # Wait a minute before retrying on error
async def do_sync(self) -> None: async def do_sync(self) -> None:
if not self.config["track_users"]: if not self.config["track_users"]:
@@ -199,16 +225,57 @@ class CommunityBot(Plugin):
# if we haven't exited by now, we must not be banned! # if we haven't exited by now, we must not be banned!
return is_banned return is_banned
async def get_messages_to_redact(self, room_id, mxid):
try:
messages = await self.client.get_messages(
room_id,
limit=100,
filter_json={
"senders": [mxid],
"not_types": ["m.room.redaction"]
},
direction=PaginationDirection.BACKWARD
)
# Filter out events with empty content
filtered_events = [event for event in messages.events if event.content and event.content.serialize()]
self.log.debug(f"DEBUG found {len(filtered_events)} messages to redact in {room_id} (after filtering empty content)")
return filtered_events
except Exception as e:
self.log.error(f"Error getting messages to redact: {e}")
return []
async def redact_messages(self, room_id):
counters = {'success': 0, 'failure': 0}
sleep_time = self.config['sleep']
events = await self.database.fetch(
"SELECT event_id FROM redaction_tasks WHERE room_id = $1",
room_id
)
for event in events:
try:
await self.client.redact(room_id, event['event_id'], reason="content removed")
counters['success'] += 1
await self.database.execute(
"DELETE FROM redaction_tasks WHERE event_id = $1",
event['event_id']
)
await asyncio.sleep(sleep_time)
except Exception as e:
if "Too Many Requests" in str(e):
self.log.warning(f"Rate limited while redacting messages in {room_id}, will try again in next loop")
return counters
self.log.error(f"Failed to redact message: {e}")
counters['failure'] += 1
await asyncio.sleep(sleep_time)
return counters
async def ban_this_user(self, user, reason="banned", all_rooms=False): async def ban_this_user(self, user, reason="banned", all_rooms=False):
#self.log.debug(f"DEBUG getting list of rooms")
roomlist = await self.get_space_roomlist() roomlist = await self.get_space_roomlist()
# don't forget to kick from the space itself # don't forget to kick from the space itself
roomlist.append(self.config["parent_room"]) 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':{}, 'error_list':{}}
ban_event_map['ban_list'][user] = [] ban_event_map['ban_list'][user] = []
#self.log.debug(f"DEBUG banning {user} from rooms...")
for room in roomlist: for room in roomlist:
try: try:
roomname = None roomname = None
@@ -234,6 +301,17 @@ class CommunityBot(Plugin):
ban_event_map['error_list'][user] = [] ban_event_map['error_list'][user] = []
ban_event_map['error_list'][user].append(roomname or room) ban_event_map['error_list'][user].append(roomname or room)
if self.config["redact_on_ban"]:
messages = await self.get_messages_to_redact(room, user)
# Queue messages for redaction
for msg in messages:
await self.database.execute(
"INSERT INTO redaction_tasks (event_id, room_id) VALUES ($1, $2)",
msg.event_id,
room
)
self.log.info(f"Queued {len(messages)} messages for redaction in {roomname or room}")
return ban_event_map return ban_event_map
async def get_banlist_roomids(self): async def get_banlist_roomids(self):
@@ -247,7 +325,7 @@ class CommunityBot(Plugin):
time.sleep(self.config['sleep']) time.sleep(self.config['sleep'])
#self.log.debug(f"DEBUG banlist id resolves to: {list_id}") #self.log.debug(f"DEBUG banlist id resolves to: {list_id}")
except: except:
evt.reply("i don't recognize that list, sorry") self.log.error(f"Banlist fetching failed for {l}")
return return
else: else:
list_id = l list_id = l
@@ -629,6 +707,32 @@ class CommunityBot(Plugin):
else: else:
await evt.reply("lol you don't have permission to do that") await evt.reply("lol you don't have permission to do that")
@community.subcommand("redact", help="redact messages from a specific user (optionally in a specific room)")
@command.argument("mxid", "full matrix ID", required=True)
@command.argument("room", "room ID", required=False)
async def mark_for_redaction(self, evt: MessageEvent, mxid: UserID, room: str) -> None:
await evt.mark_read()
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
if room:
if room.startswith('#'):
room_id = await self.client.resolve_room_alias(room)
room_id = room_id["room_id"]
else:
room_id = room
else:
room_id = evt.room_id
# get list of messages to redact in this room
messages = await self.get_messages_to_redact(room_id, mxid)
for msg in messages:
await self.database.execute(
"INSERT INTO redaction_tasks (event_id, room_id) VALUES ($1, $2)",
msg.event_id,
room_id
)
await evt.respond(f"Queued {len(messages)} messages for redaction in {room_id}")
else:
await evt.reply("lol you don't have permission to do that")
@community.subcommand("createroom", help="create a new room titled <roomname> and add it to the parent space. \ @community.subcommand("createroom", help="create a new room titled <roomname> and add it to the parent space. \
optionally include `--encrypt` to encrypt it regardless of the default settings.") optionally include `--encrypt` to encrypt it regardless of the default settings.")
+9
View File
@@ -13,3 +13,12 @@ async def upgrade_v1(conn: Connection) -> None:
ignore_inactivity INT ignore_inactivity INT
)""" )"""
) )
@upgrade_table.register(description="Include message redaction tracking")
async def upgrade_v2(conn: Connection) -> None:
await conn.execute(
"""CREATE TABLE redaction_tasks (
event_id TEXT PRIMARY KEY,
room_id TEXT NOT NULL
)"""
)
+1 -1
View File
@@ -1,6 +1,6 @@
maubot: 0.1.0 maubot: 0.1.0
id: org.jobmachine.communitybot id: org.jobmachine.communitybot
version: 0.1.17 version: 0.1.18
license: MIT license: MIT
modules: modules:
- community - community