diff --git a/base-config.yaml b/base-config.yaml
index 83f80d0..fa22ef3 100644
--- a/base-config.yaml
+++ b/base-config.yaml
@@ -27,11 +27,13 @@ track_messages: True
track_reactions: True
# list of users who can use administrative commands. these users will also be made room admins (PL100)
+# DEPRECATED: set powerlevels in the parent room instead.
admins:
- '@user1:server.tld'
- '@user2:server.tld'
# list of users who should be considered community moderators. these users will be made room mods (PL50)
+# DEPRECATED: set powerlevels in the parent room instead.
moderators:
- '@user3:server.tld'
- '@user4:server.tld'
diff --git a/community/bot.py b/community/bot.py
index c58f610..236c37d 100644
--- a/community/bot.py
+++ b/community/bot.py
@@ -7,21 +7,43 @@ import re
import fnmatch
import asyncio
-from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream
-from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType,
- MediaMessageEventContent, ReactionEvent, RedactionEvent, RoomID,
- RoomAlias, PowerLevelStateEventContent, MessageType, PaginationDirection)
+from mautrix.client import (
+ Client,
+ InternalEventType,
+ MembershipEventDispatcher,
+ SyncStream,
+)
+from mautrix.types import (
+ Event,
+ StateEvent,
+ EventID,
+ UserID,
+ FileInfo,
+ EventType,
+ MediaMessageEventContent,
+ ReactionEvent,
+ RedactionEvent,
+ RoomID,
+ RoomAlias,
+ PowerLevelStateEventContent,
+ MessageType,
+ PaginationDirection,
+ SpaceChildStateEventContent,
+ SpaceParentStateEventContent,
+ JoinRulesStateEventContent,
+ JoinRule,
+)
from mautrix.errors import MNotFound
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent
from maubot.handlers import command, event
+
BAN_STATE_EVENT = EventType.find("m.policy.rule.user", EventType.Class.STATE)
# database table related things
from .db import upgrade_table
-
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("sleep")
@@ -68,11 +90,11 @@ class CommunityBot(Plugin):
async def user_permitted(self, user_id: UserID, min_level: int = 50) -> bool:
"""Check if a user has sufficient power level in the parent room.
-
+
Args:
user_id: The Matrix ID of the user to check
min_level: Minimum required power level (default 50 for moderator)
-
+
Returns:
bool: True if user has sufficient power level
"""
@@ -94,7 +116,7 @@ class CommunityBot(Plugin):
"SELECT DISTINCT room_id FROM redaction_tasks"
)
for room in rooms:
- await self.redact_messages(room['room_id'])
+ await self.redact_messages(room["room_id"])
await asyncio.sleep(60) # Run every minute
except asyncio.CancelledError:
break
@@ -106,15 +128,17 @@ class CommunityBot(Plugin):
if not self.config["track_users"]:
return "user tracking is disabled"
- space_members_obj = await self.client.get_joined_members(self.config["parent_room"])
+ space_members_obj = await self.client.get_joined_members(
+ self.config["parent_room"]
+ )
space_members_list = space_members_obj.keys()
table_users = await self.database.fetch("SELECT mxid FROM user_events")
- table_user_list = [ row["mxid"] for row in table_users ]
+ table_user_list = [row["mxid"] for row in table_users]
untracked_users = set(space_members_list) - set(table_user_list)
non_space_members = set(table_user_list) - set(space_members_list)
results = {}
- results['added'] = []
- results['dropped'] = []
+ results["added"] = []
+ results["dropped"] = []
try:
for user in untracked_users:
now = int(time.time() * 1000)
@@ -123,12 +147,16 @@ class CommunityBot(Plugin):
VALUES ($1, $2)
"""
await self.database.execute(q, user, now)
- results['added'].append(user)
+ results["added"].append(user)
self.log.info(f"{user} inserted into activity tracking table")
for user in non_space_members:
- await self.database.execute("DELETE FROM user_events WHERE mxid = $1", user)
- self.log.info(f"{user} is not a space member, dropped from activity tracking table")
- results['dropped'].append(user)
+ await self.database.execute(
+ "DELETE FROM user_events WHERE mxid = $1", user
+ )
+ self.log.info(
+ f"{user} is not a space member, dropped from activity tracking table"
+ )
+ results["dropped"].append(user)
except Exception as e:
self.log.exception(e)
@@ -149,8 +177,8 @@ class CommunityBot(Plugin):
async def generate_report(self) -> None:
now = int(time.time() * 1000)
- warn_days_ago = (now - (1000 * 60 * 60 * 24 * self.config["warn_threshold_days"]))
- kick_days_ago = (now - (1000 * 60 * 60 * 24 * self.config["kick_threshold_days"]))
+ warn_days_ago = now - (1000 * 60 * 60 * 24 * self.config["warn_threshold_days"])
+ kick_days_ago = now - (1000 * 60 * 60 * 24 * self.config["kick_threshold_days"])
warn_q = """
SELECT mxid FROM user_events WHERE last_message_timestamp <= $1 AND
last_message_timestamp >= $2
@@ -163,24 +191,34 @@ class CommunityBot(Plugin):
ignored_q = """
SELECT mxid FROM user_events WHERE ignore_inactivity = 1
"""
- warn_inactive_results = await self.database.fetch(warn_q, warn_days_ago, kick_days_ago)
+ warn_inactive_results = await self.database.fetch(
+ warn_q, warn_days_ago, kick_days_ago
+ )
kick_inactive_results = await self.database.fetch(kick_q, kick_days_ago)
ignored_results = await self.database.fetch(ignored_q)
report = {}
- report["warn_inactive"] = [ row["mxid"] for row in warn_inactive_results ] or ["none"]
- report["kick_inactive"] = [ row["mxid"] for row in kick_inactive_results ] or ["none"]
- report["ignored"] = [ row["mxid"] for row in ignored_results ] or ["none"]
+ report["warn_inactive"] = [row["mxid"] for row in warn_inactive_results] or [
+ "none"
+ ]
+ report["kick_inactive"] = [row["mxid"] for row in kick_inactive_results] or [
+ "none"
+ ]
+ report["ignored"] = [row["mxid"] for row in ignored_results] or ["none"]
return report
def flag_message(self, msg):
- if msg.content.msgtype in [MessageType.FILE, MessageType.IMAGE, MessageType.VIDEO]:
- return self.config['censor_files']
+ if msg.content.msgtype in [
+ MessageType.FILE,
+ MessageType.IMAGE,
+ MessageType.VIDEO,
+ ]:
+ return self.config["censor_files"]
- for w in self.config['censor_wordlist']:
+ 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
@@ -188,24 +226,23 @@ class CommunityBot(Plugin):
self.log.error(f"Could not parse message for flagging: {e}")
def flag_instaban(self, msg):
- for w in self.config['censor_wordlist_instaban']:
+ for w in self.config["censor_wordlist_instaban"]:
try:
if bool(re.search(w, msg.content.body, re.IGNORECASE)):
- #self.log.debug(f"DEBUG message flagged for instaban")
+ # self.log.debug(f"DEBUG message flagged for instaban")
return True
else:
pass
except Exception as e:
self.log.error(f"Could not parse message for flagging: {e}")
-
def censor_room(self, msg):
- if isinstance(self.config['censor'], bool):
- #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")
+ if isinstance(self.config["censor"], bool):
+ # 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")
return True
else:
return False
@@ -215,113 +252,140 @@ class CommunityBot(Plugin):
is_banned = False
myrooms = await self.client.get_joined_rooms()
banlist_roomids = await self.get_banlist_roomids()
-
+
for list_id in banlist_roomids:
if list_id not in myrooms:
- self.log.error(f"Bot must be in {list_id} before attempting to use it as a banlist.")
+ self.log.error(
+ f"Bot must be in {list_id} before attempting to use it as a banlist."
+ )
pass
- #self.log.debug(f"DEBUG looking up state in {list_id}")
+ # 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}")
+ # 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}")
+ 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']}")
+ # 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']}!")
+ 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.debug(f"Found something funny in the banlist {list_id} for {rule['content']}: {e}")
+ self.log.debug(
+ f"Found something funny in the banlist {list_id} for {rule['content']}: {e}"
+ )
pass
# if we haven't exited by now, we must not be 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_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)")
+ 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']
+ 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
+ "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.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']
+ "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")
+ 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
+ counters["failure"] += 1
await asyncio.sleep(sleep_time)
return counters
- async def check_bot_permissions(self, room_id: str, evt: MessageEvent = None, required_permissions: list[str] = None) -> tuple[bool, str, dict]:
+ async def check_bot_permissions(
+ self,
+ room_id: str,
+ evt: MessageEvent = None,
+ required_permissions: list[str] = None,
+ ) -> tuple[bool, str, dict]:
"""Check if the bot has necessary permissions in a room.
-
+
Args:
room_id: The ID of the room to check permissions in
evt: Optional MessageEvent for progress updates
required_permissions: List of specific permissions to check. If None, checks basic room access.
-
+
Returns:
tuple: (bool, str, dict) - (has_permissions, error_message, permission_details)
"""
try:
# Check if bot is in the room
try:
- await self.client.get_state_event(room_id, EventType.ROOM_MEMBER, self.client.mxid)
+ await self.client.get_state_event(
+ room_id, EventType.ROOM_MEMBER, self.client.mxid
+ )
except MNotFound:
return False, "Bot is not a member of this room", {}
# Get power levels
- power_levels = await self.client.get_state_event(room_id, EventType.ROOM_POWER_LEVELS)
- bot_level = power_levels.users.get(self.client.mxid, power_levels.users_default)
-
+ power_levels = await self.client.get_state_event(
+ room_id, EventType.ROOM_POWER_LEVELS
+ )
+ bot_level = power_levels.users.get(
+ self.client.mxid, power_levels.users_default
+ )
+
# Define required power levels for different actions
permission_requirements = {
"redact": power_levels.redact,
"kick": power_levels.kick,
"ban": power_levels.ban,
"invite": power_levels.invite,
- "tombstone": power_levels.events.get("m.room.tombstone", power_levels.events_default),
- "power_levels": power_levels.events.get("m.room.power_levels", power_levels.events_default),
- "state": power_levels.state_default
+ "tombstone": power_levels.events.get(
+ "m.room.tombstone", power_levels.events_default
+ ),
+ "power_levels": power_levels.events.get(
+ "m.room.power_levels", power_levels.events_default
+ ),
+ "state": power_levels.state_default,
}
-
+
# Check each required permission
permission_status = {}
if required_permissions:
@@ -331,23 +395,32 @@ class CommunityBot(Plugin):
permission_status[perm] = {
"has_permission": bot_level >= required_level,
"required_level": required_level,
- "bot_level": bot_level
+ "bot_level": bot_level,
}
-
+
# If no specific permissions requested, just check basic access
if not required_permissions:
if bot_level < 50: # Basic moderator level
- return False, "Bot does not have sufficient power level (needs at least moderator level)", permission_status
+ return (
+ False,
+ "Bot does not have sufficient power level (needs at least moderator level)",
+ permission_status,
+ )
return True, "", permission_status
-
+
# Check if all requested permissions are granted
- missing_permissions = [perm for perm, status in permission_status.items()
- if not status["has_permission"]]
-
+ missing_permissions = [
+ perm
+ for perm, status in permission_status.items()
+ if not status["has_permission"]
+ ]
+
if missing_permissions:
- error_msg = "Bot is missing required permissions: " + ", ".join(missing_permissions)
+ error_msg = "Bot is missing required permissions: " + ", ".join(
+ missing_permissions
+ )
return False, error_msg, permission_status
-
+
return True, "", permission_status
except Exception as e:
@@ -357,23 +430,23 @@ class CommunityBot(Plugin):
await evt.respond(error_msg)
return False, error_msg, {}
- async def do_archive_room(self, room_id: str, evt: MessageEvent = None, replacement_room: str = "") -> bool:
+ async def do_archive_room(
+ self, room_id: str, evt: MessageEvent = None, replacement_room: str = ""
+ ) -> bool:
"""Handle common room archival activities like removing from space, removing aliases, and setting tombstone.
-
+
Args:
room_id: The ID of the room to archive
evt: Optional MessageEvent for progress updates
replacement_room: Optional room ID to point to in the tombstone event
-
+
Returns:
bool: True if all operations succeeded, False otherwise
"""
try:
# Check permissions for all required operations
has_perms, error_msg, _ = await self.check_bot_permissions(
- room_id,
- evt,
- ["state", "tombstone", "power_levels"]
+ room_id, evt, ["state", "tombstone", "power_levels"]
)
if not has_perms:
if evt:
@@ -386,35 +459,43 @@ class CommunityBot(Plugin):
room_id=room_id,
event_type="m.space.parent",
content={}, # Empty content removes the state
- state_key=self.config["parent_room"]
+ state_key=self.config["parent_room"],
)
self.log.info(f"Removed parent space reference from room {room_id}")
# Remove the child reference from the space
- self.log.debug(f"DEBUG removing child state reference from {self.config['parent_room']}")
+ self.log.debug(
+ f"DEBUG removing child state reference from {self.config['parent_room']}"
+ )
await self.client.send_state_event(
- self.config["parent_room"],
+ self.config["parent_room"],
event_type="m.space.child",
content={}, # Empty content removes the state
- state_key=room_id
+ state_key=room_id,
+ )
+ self.log.info(
+ f"Removed child room reference from space {self.config['parent_room']}"
)
- self.log.info(f"Removed child room reference from space {self.config['parent_room']}")
# Remove room aliases to release them
await self.remove_room_aliases(room_id, evt)
# Send the tombstone
tombstone_content = {
- "body": "This room has been archived." if not replacement_room else "This room has been replaced. Please join the new room.",
- "replacement_room": replacement_room
+ "body": (
+ "This room has been archived."
+ if not replacement_room
+ else "This room has been replaced. Please join the new room."
+ ),
+ "replacement_room": replacement_room,
}
await self.client.send_state_event(
room_id=room_id,
event_type=EventType.ROOM_TOMBSTONE,
- content=tombstone_content
+ content=tombstone_content,
)
self.log.info(f"Successfully added tombstone to room {room_id}")
-
+
return True
except Exception as e:
@@ -426,11 +507,11 @@ class CommunityBot(Plugin):
async def remove_room_aliases(self, room_id: str, evt: MessageEvent = None) -> list:
"""Remove all aliases from a room.
-
+
Args:
room_id: The ID of the room whose aliases to remove
evt: Optional MessageEvent for progress updates
-
+
Returns:
list: List of aliases that were successfully removed
"""
@@ -438,18 +519,17 @@ class CommunityBot(Plugin):
try:
aliases = await self.client.get_state_event(
- room_id=room_id,
- event_type=EventType.ROOM_CANONICAL_ALIAS
+ room_id=room_id, event_type=EventType.ROOM_CANONICAL_ALIAS
)
except Exception as e:
self.log.warning(f"Failed to get room alias state event, skipping: {e}")
return removed_aliases
-
+
if aliases.alt_aliases:
for alias in aliases.alt_aliases:
try:
await self.client.remove_room_alias(
- alias_localpart=alias.split(':')[0].lstrip('#'),
+ alias_localpart=alias.split(":")[0].lstrip("#"),
)
self.log.info(f"Removed alias {alias} from room {room_id}")
removed_aliases.append(alias)
@@ -459,9 +539,11 @@ class CommunityBot(Plugin):
if aliases.canonical_alias:
try:
await self.client.remove_room_alias(
- alias_localpart=aliases.canonical_alias.split(':')[0].lstrip('#'),
+ alias_localpart=aliases.canonical_alias.split(":")[0].lstrip("#"),
+ )
+ self.log.info(
+ f"Removed canonical alias {aliases.canonical_alias} from room {room_id}"
)
- self.log.info(f"Removed canonical alias {aliases.canonical_alias} from room {room_id}")
removed_aliases.append(aliases.canonical_alias)
except Exception as e:
self.log.warning(f"Failed to remove canonical alias: {e}")
@@ -472,14 +554,14 @@ class CommunityBot(Plugin):
roomlist = await self.get_space_roomlist()
# don't forget to kick from the space itself
roomlist.append(self.config["parent_room"])
- 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] = []
for room in roomlist:
try:
roomname = None
- roomnamestate = await self.client.get_state_event(room, 'm.room.name')
- roomname = roomnamestate['name']
+ roomnamestate = await self.client.get_state_event(room, "m.room.name")
+ roomname = roomnamestate["name"]
# ban user even if they're not in the room!
if all_rooms:
@@ -489,16 +571,16 @@ class CommunityBot(Plugin):
await self.client.ban_user(room, user, reason=reason)
if roomname:
- ban_event_map['ban_list'][user].append(roomname)
+ ban_event_map["ban_list"][user].append(roomname)
else:
- ban_event_map['ban_list'][user].append(room)
- time.sleep(self.config['sleep'])
+ ban_event_map["ban_list"][user].append(room)
+ time.sleep(self.config["sleep"])
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)
+ ban_event_map["error_list"][user] = []
+ 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)
@@ -507,22 +589,24 @@ class CommunityBot(Plugin):
await self.database.execute(
"INSERT INTO redaction_tasks (event_id, room_id) VALUES ($1, $2)",
msg.event_id,
- room
+ room,
)
- self.log.info(f"Queued {len(messages)} messages for redaction in {roomname or room}")
+ self.log.info(
+ f"Queued {len(messages)} messages for redaction in {roomname or room}"
+ )
return ban_event_map
async def get_banlist_roomids(self):
banlist_roomids = []
- for l in self.config['banlists']:
- #self.log.debug(f"DEBUG getting banlist {l}")
- if l.startswith('#'):
+ 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"]
- time.sleep(self.config['sleep'])
- #self.log.debug(f"DEBUG banlist id resolves to: {list_id}")
+ time.sleep(self.config["sleep"])
+ # self.log.debug(f"DEBUG banlist id resolves to: {list_id}")
except:
self.log.error(f"Banlist fetching failed for {l}")
return
@@ -533,9 +617,8 @@ class CommunityBot(Plugin):
return banlist_roomids
-
@event.on(BAN_STATE_EVENT)
- async def check_ban_event(self, evt:StateEvent) -> None:
+ async def check_ban_event(self, evt: StateEvent) -> None:
if not self.config["proactive_banning"]:
return
@@ -547,11 +630,15 @@ class CommunityBot(Plugin):
try:
entity = evt.content["entity"]
recommendation = evt.content["recommendation"]
- self.log.debug(f"DEBUG new ban rule found: {entity} should have action {recommendation}")
+ self.log.debug(
+ f"DEBUG new ban rule found: {entity} should have action {recommendation}"
+ )
if bool(re.search(r"[*?]", entity)):
- self.log.debug(f"DEBUG ban rule appears to be glob pattern, skipping proactive measures.")
+ self.log.debug(
+ f"DEBUG ban rule appears to be glob pattern, skipping proactive measures."
+ )
return
- if bool(re.search('ban$', recommendation)):
+ if bool(re.search("ban$", recommendation)):
await self.ban_this_user(entity)
except Exception as e:
self.log.error(e)
@@ -559,14 +646,14 @@ class CommunityBot(Plugin):
@event.on(EventType.ROOM_POWER_LEVELS)
async def sync_power_levels(self, evt: StateEvent) -> None:
# Only care about changes in the parent room
- if evt.room_id != self.config['parent_room']:
+ if evt.room_id != self.config["parent_room"]:
return
# Get the changed user and their new power level
try:
old_levels = evt.prev_content.get("users", {})
new_levels = evt.content.get("users", {})
-
+
# Find which user's power level changed
changed_users = {}
for user, new_level in new_levels.items():
@@ -583,69 +670,82 @@ class CommunityBot(Plugin):
# Apply the same power level changes to each room
for room_id in space_rooms:
- if room_id == self.config['parent_room']:
+ if room_id == self.config["parent_room"]:
continue
try:
- roomname = (await self.client.get_state_event(room_id, "m.room.name"))["name"]
+ roomname = (
+ await self.client.get_state_event(room_id, "m.room.name")
+ )["name"]
except:
self.log.warning(f"Unable to get room name for {room_id}")
# Get current power levels
try:
# Get current power levels
- current_pl = await self.client.get_state_event(room_id, EventType.ROOM_POWER_LEVELS)
-
+ current_pl = await self.client.get_state_event(
+ room_id, EventType.ROOM_POWER_LEVELS
+ )
+
# Update existing power levels object with new levels
users = current_pl.get("users", {})
for user, level in changed_users.items():
users[user] = level
-
+
current_pl["users"] = users
-
+
# Send updated power levels
try:
- await self.client.send_state_event(room_id, EventType.ROOM_POWER_LEVELS, current_pl)
+ await self.client.send_state_event(
+ room_id, EventType.ROOM_POWER_LEVELS, current_pl
+ )
success_rooms.append(roomname or room_id)
except Exception as e:
- self.log.error(f"Failed to send power levels to {roomname or room_id}: {e}")
+ self.log.error(
+ f"Failed to send power levels to {roomname or room_id}: {e}"
+ )
failed_rooms.append(roomname or room_id)
-
- time.sleep(self.config['sleep'])
-
+
+ time.sleep(self.config["sleep"])
+
except Exception as e:
self.log.warning(f"Failed to update power levels in {room_id}: {e}")
failed_rooms.append(room_id)
# Send notification if configured
if self.config["notification_room"]:
- changes = ", ".join([f"{user} → {level}" for user, level in changed_users.items()])
- notification = f"Power level changes ({changes}) propagated from parent room:
"
- notification += f"Succeeded in: {', '.join(success_rooms)}
"
+ changes = ", ".join(
+ [f"{user} → {level}" for user, level in changed_users.items()]
+ )
+ notification = (
+ f"Power level changes ({changes}) propagated from parent room:
"
+ )
+ notification += (
+ f"Succeeded in: {', '.join(success_rooms)}
"
+ )
if failed_rooms:
notification += f"Failed in: {', '.join(failed_rooms)}"
-
+
await self.client.send_notice(
- self.config["notification_room"],
- html=notification
+ self.config["notification_room"], html=notification
)
except Exception as e:
self.log.error(f"Error syncing power levels: {e}")
-
+
@event.on(InternalEventType.JOIN)
- async def newjoin(self, evt:StateEvent) -> None:
+ 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!")
+ # 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']:
+ if evt.room_id == self.config["parent_room"]:
await self.do_sync()
# greeting activities
room_id = str(evt.room_id)
@@ -653,48 +753,69 @@ class CommunityBot(Plugin):
# just in case we got here even if the person is on the banlists
if on_banlist:
return
- greeting_map = self.config['greetings']
- greeting_name = self.config['greeting_rooms'][room_id]
+ greeting_map = self.config["greetings"]
+ greeting_name = self.config["greeting_rooms"][room_id]
nick = self.client.parse_user_id(evt.sender)[0]
- pill = '{nick}'.format(mxid=evt.sender, nick=nick)
+ pill = '{nick}'.format(
+ mxid=evt.sender, nick=nick
+ )
if greeting_name != "none":
greeting = greeting_map[greeting_name].format(user=pill)
- time.sleep(self.config['welcome_sleep'])
- await self.client.send_notice(evt.room_id, html=greeting)
+ time.sleep(self.config["welcome_sleep"])
+ await self.client.send_notice(evt.room_id, html=greeting)
else:
pass
if self.config["notification_room"]:
- roomnamestate = await self.client.get_state_event(evt.room_id, 'm.room.name')
- roomname = roomnamestate['name']
- notification_message = self.config['join_notification_message'].format(user=evt.sender,
- room=roomname)
- await self.client.send_notice(self.config["notification_room"], html=notification_message)
+ roomnamestate = await self.client.get_state_event(
+ evt.room_id, "m.room.name"
+ )
+ roomname = roomnamestate["name"]
+ notification_message = self.config[
+ "join_notification_message"
+ ].format(user=evt.sender, room=roomname)
+ await self.client.send_notice(
+ self.config["notification_room"], html=notification_message
+ )
@event.on(EventType.ROOM_MESSAGE)
async def update_message_timestamp(self, evt: MessageEvent) -> None:
- power_levels = await self.client.get_state_event(evt.room_id, EventType.ROOM_POWER_LEVELS)
+ power_levels = await self.client.get_state_event(
+ evt.room_id, EventType.ROOM_POWER_LEVELS
+ )
user_level = power_levels.get_user_level(evt.sender)
- #self.log.debug(f"DEBUGDEBUG user {evt.sender} has power level {user_level}")
+ # self.log.debug(f"DEBUGDEBUG user {evt.sender} has power level {user_level}")
if self.flag_message(evt):
# do we need to redact?
- if not await self.user_permitted(evt.sender) and \
- evt.sender != self.client.mxid and \
- self.censor_room(evt):
+ if (
+ not await self.user_permitted(evt.sender)
+ and evt.sender != self.client.mxid
+ and self.censor_room(evt)
+ ):
try:
- await self.client.redact(evt.room_id, evt.event_id, reason="message flagged")
+ await self.client.redact(
+ evt.room_id, evt.event_id, reason="message flagged"
+ )
except Exception as e:
self.log.error(f"Flagged message could not be redacted: {e}")
- if evt.content.msgtype in {MessageType.TEXT, MessageType.NOTICE, MessageType.EMOTE}:
+ if evt.content.msgtype in {
+ MessageType.TEXT,
+ MessageType.NOTICE,
+ MessageType.EMOTE,
+ }:
if self.flag_instaban(evt):
# do we need to redact?
- if not await self.user_permitted(evt.sender) and \
- evt.sender != self.client.mxid and \
- self.censor_room(evt):
+ if (
+ not await self.user_permitted(evt.sender)
+ and evt.sender != self.client.mxid
+ and self.censor_room(evt)
+ ):
try:
- await self.client.redact(evt.room_id, evt.event_id, reason="message flagged")
+ await self.client.redact(
+ evt.room_id, evt.event_id, reason="message flagged"
+ )
except Exception as e:
self.log.error(f"Flagged message could not be redacted: {e}")
-
+
await self.ban_this_user(evt.sender, all_rooms=True)
if not self.config["track_messages"] or not self.config["track_users"]:
@@ -737,15 +858,19 @@ class CommunityBot(Plugin):
async def community(self) -> None:
pass
- @community.subcommand("bancheck", help="check subscribed banlists for a user's mxid")
+ @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")
+ @community.subcommand(
+ "sync",
+ help="update the activity tracker with the current space members \
+ in case they are missing",
+ )
async def sync_space_members(self, evt: MessageEvent) -> None:
if not await self.user_permitted(evt.sender):
await evt.reply("You don't have permission to use this command")
@@ -756,10 +881,14 @@ class CommunityBot(Plugin):
# we can skip all of this logic if those config values are empty
# this logic helps migrate explicit configuration to the parent-room inheritance model
if not self.config["admins"] and not self.config["moderators"]:
- self.log.info("no admins or moderators configured, skipping power level sync")
+ self.log.info(
+ "no admins or moderators configured, skipping power level sync"
+ )
pass
else:
- power_levels = await self.client.get_state_event(self.config["parent_room"], EventType.ROOM_POWER_LEVELS)
+ power_levels = await self.client.get_state_event(
+ self.config["parent_room"], EventType.ROOM_POWER_LEVELS
+ )
users = power_levels.get("users", {})
for user in self.config["admins"]:
if user not in users or users.get(user) < 100:
@@ -774,7 +903,11 @@ class CommunityBot(Plugin):
try:
# update full powerlevels object with updated user object
power_levels["users"] = users
- await self.client.send_state_event(self.config["parent_room"], EventType.ROOM_POWER_LEVELS, power_levels)
+ await self.client.send_state_event(
+ self.config["parent_room"],
+ EventType.ROOM_POWER_LEVELS,
+ power_levels,
+ )
# if updating was successful, let's go ahead and clear out the values in the config
self.config["admins"] = []
self.config["moderators"] = []
@@ -782,132 +915,154 @@ class CommunityBot(Plugin):
self.config.save()
self.log.debug("successfully migrated admin/mod config to parent room")
except Exception as e:
- self.log.error(f"Failed to send power levels to {self.config["parent_room"]}: {e}")
- await evt.respond(f"Failed to send power levels to {self.config["parent_room"]}: {e}")
-
-
+ self.log.error(
+ f"Failed to send power levels to {self.config["parent_room"]}: {e}"
+ )
+ await evt.respond(
+ f"Failed to send power levels to {self.config["parent_room"]}: {e}"
+ )
- if not self.config["track_users"]:
- await evt.respond("user tracking is disabled")
- return
+ if not self.config["track_users"]:
+ await evt.respond("user tracking is disabled")
+ return
- results = await self.do_sync()
+ results = await self.do_sync()
- added_str = "
".join(results['added'])
- dropped_str = "
".join(results['dropped'])
- await evt.respond(f"Added: {added_str}
Dropped: {dropped_str}", allow_html=True)
+ added_str = "
".join(results["added"])
+ dropped_str = "
".join(results["dropped"])
+ await evt.respond(
+ f"Added: {added_str}
Dropped: {dropped_str}", allow_html=True
+ )
-
- @community.subcommand("ignore", help="exclude a specific matrix ID from inactivity tracking")
+ @community.subcommand(
+ "ignore", help="exclude a specific matrix ID from inactivity tracking"
+ )
@command.argument("mxid", "full matrix ID", required=True)
async def ignore_inactivity(self, evt: MessageEvent, mxid: UserID) -> None:
if not await self.user_permitted(evt.sender):
await evt.reply("You don't have permission to use this command")
return
- if not self.config["track_users"]:
- await evt.reply("user tracking is disabled")
- return
+ if not self.config["track_users"]:
+ await evt.reply("user tracking is disabled")
+ return
- try:
- Client.parse_user_id(mxid)
- await self.database.execute("UPDATE user_events SET ignore_inactivity = 1 WHERE \
- mxid = $1", mxid)
- self.log.info(f"{mxid} set to ignore inactivity")
- await evt.react("✅")
- except Exception as e:
- await evt.respond(f"{e}")
+ try:
+ Client.parse_user_id(mxid)
+ await self.database.execute(
+ "UPDATE user_events SET ignore_inactivity = 1 WHERE \
+ mxid = $1",
+ mxid,
+ )
+ self.log.info(f"{mxid} set to ignore inactivity")
+ await evt.react("✅")
+ except Exception as e:
+ await evt.respond(f"{e}")
- @community.subcommand("unignore", help="re-enable activity tracking for a specific matrix ID")
+ @community.subcommand(
+ "unignore", help="re-enable activity tracking for a specific matrix ID"
+ )
@command.argument("mxid", "full matrix ID", required=True)
async def unignore_inactivity(self, evt: MessageEvent, mxid: UserID) -> None:
if not await self.user_permitted(evt.sender):
await evt.reply("You don't have permission to use this command")
return
- if not self.config["track_users"]:
- await evt.reply("user tracking is disabled")
- return
+ if not self.config["track_users"]:
+ await evt.reply("user tracking is disabled")
+ return
- try:
- Client.parse_user_id(mxid)
- await self.database.execute("UPDATE user_events SET ignore_inactivity = 0 WHERE \
- mxid = $1", mxid)
- self.log.info(f"{mxid} set to track inactivity")
- await evt.react("✅")
- except Exception as e:
- await evt.respond(f"{e}")
+ try:
+ Client.parse_user_id(mxid)
+ await self.database.execute(
+ "UPDATE user_events SET ignore_inactivity = 0 WHERE \
+ mxid = $1",
+ mxid,
+ )
+ self.log.info(f"{mxid} set to track inactivity")
+ await evt.react("✅")
+ except Exception as e:
+ await evt.respond(f"{e}")
- @community.subcommand("report", help='generate a list of matrix IDs that have been inactive')
+ @community.subcommand(
+ "report", help="generate a list of matrix IDs that have been inactive"
+ )
async def get_report(self, evt: MessageEvent) -> None:
if not await self.user_permitted(evt.sender):
await evt.reply("You don't have permission to use this command")
return
- if not self.config["track_users"]:
- await evt.reply("user tracking is disabled")
- return
+ if not self.config["track_users"]:
+ await evt.reply("user tracking is disabled")
+ return
- sync_results = await self.do_sync()
- report = await self.generate_report()
- await evt.respond(f"
Users inactive for between {self.config['warn_threshold_days']} and \
- {self.config['kick_threshold_days']} days:
\
- {'
'.join(report['warn_inactive'])}
Users inactive for at least {self.config['kick_threshold_days']} days:
\
- {'
'.join(report['kick_inactive'])}
Ignored users:
\
- {'
'.join(report['ignored'])}
Users inactive for between {self.config['warn_threshold_days']} and \
+ {self.config['kick_threshold_days']} days:
\
+ {'
'.join(report['warn_inactive'])}
Users inactive for at least {self.config['kick_threshold_days']} days:
\
+ {'
'.join(report['kick_inactive'])}
Ignored users:
\
+ {'
'.join(report['ignored'])}
{purge_list}
{error_list}
{purge_list}
{error_list}
{purge_list}
{error_list}
{purge_list}
{error_list}
{ban_list}
{error_list}
{ban_list}
{error_list}
{unban_list}
{error_list}
{unban_list}
{error_list}
{', '.join(success_list)}\n\n"
+ if error_list:
+ results += (
+ f"Failed to update rooms:\n{', '.join(error_list)}"
+ )
- results = "the following rooms were updated:{success_list}
{error_list}