better powerlevel handling, first pass
This commit is contained in:
+176
-36
@@ -66,6 +66,26 @@ class CommunityBot(Plugin):
|
||||
self._redaction_tasks.cancel()
|
||||
await super().stop()
|
||||
|
||||
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
|
||||
"""
|
||||
try:
|
||||
power_levels = await self.client.get_state_event(
|
||||
self.config["parent_room"], EventType.ROOM_POWER_LEVELS
|
||||
)
|
||||
user_level = power_levels.get_user_level(user_id)
|
||||
return user_level >= min_level
|
||||
except Exception as e:
|
||||
self.log.error(f"Failed to check user power level: {e}")
|
||||
return False
|
||||
|
||||
async def _redaction_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
@@ -357,6 +377,82 @@ class CommunityBot(Plugin):
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
@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']:
|
||||
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():
|
||||
if user not in old_levels or old_levels[user] != new_level:
|
||||
changed_users[user] = new_level
|
||||
|
||||
if not changed_users:
|
||||
return
|
||||
|
||||
# Get all rooms in the space
|
||||
space_rooms = await self.client.get_joined_rooms()
|
||||
success_rooms = []
|
||||
failed_rooms = []
|
||||
|
||||
# Apply the same power level changes to each room
|
||||
for room_id in space_rooms:
|
||||
if room_id == self.config['parent_room']:
|
||||
continue
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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}")
|
||||
failed_rooms.append(roomname or room_id)
|
||||
|
||||
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:<br>"
|
||||
notification += f"Succeeded in: <code>{', '.join(success_rooms)}</code><br>"
|
||||
if failed_rooms:
|
||||
notification += f"Failed in: <code>{', '.join(failed_rooms)}</code>"
|
||||
|
||||
await self.client.send_notice(
|
||||
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:
|
||||
@@ -476,7 +572,46 @@ class CommunityBot(Plugin):
|
||||
@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 evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
# check config values for admins and moderators. if they have a lower PL in the parent room,
|
||||
# attempt to update the parent room with their appropriate admin/mod status
|
||||
# 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")
|
||||
pass
|
||||
else:
|
||||
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:
|
||||
# update the users object in-place
|
||||
users[user] = 100
|
||||
|
||||
for user in self.config["moderators"]:
|
||||
if user not in users or users.get(user) < 50:
|
||||
# update the users object in-place
|
||||
users[user] = 50
|
||||
|
||||
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)
|
||||
# if updating was successful, let's go ahead and clear out the values in the config
|
||||
self.config["admins"] = []
|
||||
self.config["moderators"] = []
|
||||
# and save the config to the file
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
if not self.config["track_users"]:
|
||||
await evt.respond("user tracking is disabled")
|
||||
return
|
||||
@@ -486,14 +621,15 @@ class CommunityBot(Plugin):
|
||||
added_str = "<br />".join(results['added'])
|
||||
dropped_str = "<br />".join(results['dropped'])
|
||||
await evt.respond(f"Added: {added_str}<br /><br />Dropped: {dropped_str}", allow_html=True)
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to do that")
|
||||
|
||||
|
||||
@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 evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
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
|
||||
@@ -506,13 +642,14 @@ class CommunityBot(Plugin):
|
||||
await evt.react("✅")
|
||||
except Exception as e:
|
||||
await evt.respond(f"{e}")
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to set that")
|
||||
|
||||
@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 evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
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
|
||||
@@ -525,12 +662,13 @@ class CommunityBot(Plugin):
|
||||
await evt.react("✅")
|
||||
except Exception as e:
|
||||
await evt.respond(f"{e}")
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to set that")
|
||||
|
||||
@community.subcommand("report", help='generate a list of matrix IDs that have been inactive')
|
||||
async def get_report(self, evt: MessageEvent) -> None:
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
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
|
||||
@@ -550,7 +688,10 @@ class CommunityBot(Plugin):
|
||||
@community.subcommand("purge", help='kick users for excessive inactivity')
|
||||
async def kick_users(self, evt: MessageEvent) -> None:
|
||||
await evt.mark_read()
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
msg = await evt.respond("starting the purge...")
|
||||
report = await self.generate_report()
|
||||
purgeable = report['kick_inactive']
|
||||
@@ -590,15 +731,15 @@ class CommunityBot(Plugin):
|
||||
# sync our database after we've made changes to room memberships
|
||||
await self.do_sync()
|
||||
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to do that")
|
||||
|
||||
|
||||
@community.subcommand("kick", help='kick a specific user from the community and all rooms')
|
||||
@command.argument("mxid", "full matrix ID", required=True)
|
||||
async def kick_user(self, evt: MessageEvent, mxid: UserID) -> None:
|
||||
await evt.mark_read()
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
user = mxid
|
||||
msg = await evt.respond("starting the purge...")
|
||||
roomlist = await self.get_space_roomlist()
|
||||
@@ -636,15 +777,15 @@ class CommunityBot(Plugin):
|
||||
# sync our database after we've made changes to room memberships
|
||||
await self.do_sync()
|
||||
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to do that")
|
||||
|
||||
|
||||
@community.subcommand("ban", help='kick and ban a specific user from the community and all rooms')
|
||||
@command.argument("mxid", "full matrix ID", required=True)
|
||||
async def ban_user(self, evt: MessageEvent, mxid: UserID) -> None:
|
||||
await evt.mark_read()
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
user = mxid
|
||||
msg = await evt.respond("starting the ban...")
|
||||
results_map = await self.ban_this_user(user, all_rooms=True)
|
||||
@@ -658,15 +799,15 @@ class CommunityBot(Plugin):
|
||||
# sync our database after we've made changes to room memberships
|
||||
await self.do_sync()
|
||||
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to do that")
|
||||
|
||||
|
||||
@community.subcommand("unban", help='unban a specific user from the community and all rooms')
|
||||
@command.argument("mxid", "full matrix ID", required=True)
|
||||
async def unban_user(self, evt: MessageEvent, mxid: UserID) -> None:
|
||||
await evt.mark_read()
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
user = mxid
|
||||
msg = await evt.respond("starting the unban...")
|
||||
roomlist = await self.get_space_roomlist()
|
||||
@@ -704,15 +845,15 @@ class CommunityBot(Plugin):
|
||||
# sync our database after we've made changes to room memberships
|
||||
await self.do_sync()
|
||||
|
||||
else:
|
||||
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 not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
if room:
|
||||
if room.startswith('#'):
|
||||
room_id = await self.client.resolve_room_alias(room)
|
||||
@@ -731,8 +872,6 @@ class CommunityBot(Plugin):
|
||||
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. \
|
||||
optionally include `--encrypt` to encrypt it regardless of the default settings.")
|
||||
@@ -743,7 +882,10 @@ class CommunityBot(Plugin):
|
||||
use `--encrypt` to ensure it is encrypted at creation time even if that isnt my default \
|
||||
setting.')
|
||||
else:
|
||||
if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]:
|
||||
if not await self.user_permitted(evt.sender):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
encrypted_flag_regex = re.compile(r'(\s+|^)-+encrypt(ed)?\s?')
|
||||
force_encryption = bool(encrypted_flag_regex.search(roomname))
|
||||
try:
|
||||
@@ -794,8 +936,6 @@ class CommunityBot(Plugin):
|
||||
|
||||
except Exception as e:
|
||||
await evt.respond(f"i tried, but something went wrong: \"{e}\"", edits=mymsg)
|
||||
else:
|
||||
await evt.reply("you're not the boss of me!")
|
||||
|
||||
|
||||
#need to somehow regularly fetch and update the list of room ids that are associated with a given space
|
||||
@@ -857,7 +997,10 @@ class CommunityBot(Plugin):
|
||||
@community.subcommand("setpower", help="set power levels according to the community configuration")
|
||||
async def set_powerlevels(self, evt: MessageEvent,) -> None:
|
||||
await evt.mark_read()
|
||||
if evt.sender in self.config["admins"]:
|
||||
if not await self.user_permitted(evt.sender, min_level=100):
|
||||
await evt.reply("You don't have permission to use this command")
|
||||
return
|
||||
|
||||
msg = await evt.respond("truing up power levels, this could take a minute...")
|
||||
admins = self.config['admins']
|
||||
moderators = self.config['moderators']
|
||||
@@ -917,9 +1060,6 @@ class CommunityBot(Plugin):
|
||||
# sync our database after we've made changes to room memberships
|
||||
await self.do_sync()
|
||||
|
||||
else:
|
||||
await evt.reply("lol you don't have permission to do that")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
maubot: 0.1.0
|
||||
id: org.jobmachine.communitybot
|
||||
version: 0.1.18
|
||||
version: 0.1.19
|
||||
license: MIT
|
||||
modules:
|
||||
- community
|
||||
|
||||
Reference in New Issue
Block a user