diff --git a/community/bot.py b/community/bot.py index 39cfdb3..c58f610 100644 --- a/community/bot.py +++ b/community/bot.py @@ -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: @@ -536,6 +556,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:
" + 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 + ) + + except Exception as e: + self.log.error(f"Error syncing power levels: {e}") @event.on(InternalEventType.JOIN) async def newjoin(self, evt:StateEvent) -> None: @@ -581,9 +677,7 @@ class CommunityBot(Plugin): #self.log.debug(f"DEBUGDEBUG user {evt.sender} has power level {user_level}") if self.flag_message(evt): # do we need to redact? - if evt.sender not in self.config['admins'] and \ - evt.sender not in self.config['moderators'] and \ - user_level < self.config['uncensor_pl'] and \ + if not await self.user_permitted(evt.sender) and \ evt.sender != self.client.mxid and \ self.censor_room(evt): try: @@ -593,9 +687,7 @@ class CommunityBot(Plugin): if evt.content.msgtype in {MessageType.TEXT, MessageType.NOTICE, MessageType.EMOTE}: if self.flag_instaban(evt): # do we need to redact? - if evt.sender not in self.config['admins'] and \ - evt.sender not in self.config['moderators'] and \ - user_level < self.config['uncensor_pl'] and \ + if not await self.user_permitted(evt.sender) and \ evt.sender != self.client.mxid and \ self.censor_room(evt): try: @@ -655,137 +747,136 @@ 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 self.config["track_users"]: - await evt.respond("user tracking is disabled") - return + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - 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) + # 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: - await evt.reply("lol you don't have permission to do that") + 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 + + 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) @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 self.config["track_users"]: - await evt.reply("user tracking is disabled") - return + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + 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}") - else: - await evt.reply("lol you don't have permission to set that") + 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}") @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 self.config["track_users"]: - await evt.reply("user tracking is disabled") - return + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + 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}") - else: - await evt.reply("lol you don't have permission to set that") + 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}") @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 self.config["track_users"]: - await evt.reply("user tracking is disabled") - return + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + 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'])}

", \ - allow_html=True) + 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'])}

", \ + allow_html=True) @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"]: - msg = await evt.respond("starting the purge...") - report = await self.generate_report() - purgeable = report['kick_inactive'] - roomlist = await self.get_space_roomlist() - # don't forget to kick from the space itself - roomlist.append(self.config["parent_room"]) - purge_list = {} - error_list = {} + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - for user in purgeable: - purge_list[user] = [] - for room in roomlist: - try: - roomname = None - roomnamestate = await self.client.get_state_event(room, 'm.room.name') - roomname = roomnamestate['name'] - - await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) - await self.client.kick_user(room, user, reason='inactivity') - if roomname: - purge_list[user].append(roomname) - else: - purge_list[user].append(room) - time.sleep('sleep') - except MNotFound: - pass - except Exception as e: - self.log.warning(e) - error_list[user] = [] - error_list[user].append(roomname or room) - - - results = "the following users were purged:

{purge_list}

the following errors were \ - recorded:

{error_list}

".format(purge_list=purge_list, error_list=error_list) - await evt.respond(results, allow_html=True, edits=msg) - - # 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"]: - user = mxid - msg = await evt.respond("starting the purge...") - roomlist = await self.get_space_roomlist() - # don't forget to kick from the space itself - roomlist.append(self.config["parent_room"]) - purge_list = {} - error_list = {} + msg = await evt.respond("starting the purge...") + report = await self.generate_report() + purgeable = report['kick_inactive'] + roomlist = await self.get_space_roomlist() + # don't forget to kick from the space itself + roomlist.append(self.config["parent_room"]) + purge_list = {} + error_list = {} + for user in purgeable: purge_list[user] = [] for room in roomlist: try: @@ -794,12 +885,12 @@ class CommunityBot(Plugin): roomname = roomnamestate['name'] await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) - await self.client.kick_user(room, user, reason='kicked') + await self.client.kick_user(room, user, reason='inactivity') if roomname: purge_list[user].append(roomname) else: purge_list[user].append(room) - time.sleep(self.config['sleep']) + time.sleep('sleep') except MNotFound: pass except Exception as e: @@ -808,110 +899,154 @@ class CommunityBot(Plugin): error_list[user].append(roomname or room) - results = "the following users were kicked:

{purge_list}

the following errors were \ - recorded:

{error_list}

".format(purge_list=purge_list, error_list=error_list) - await evt.respond(results, allow_html=True, edits=msg) + results = "the following users were purged:

{purge_list}

the following errors were \ + recorded:

{error_list}

".format(purge_list=purge_list, error_list=error_list) + await evt.respond(results, allow_html=True, edits=msg) - # sync our database after we've made changes to room memberships - await self.do_sync() + # 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 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() + # don't forget to kick from the space itself + roomlist.append(self.config["parent_room"]) + purge_list = {} + error_list = {} + + purge_list[user] = [] + for room in roomlist: + try: + roomname = None + roomnamestate = await self.client.get_state_event(room, 'm.room.name') + roomname = roomnamestate['name'] + + await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) + await self.client.kick_user(room, user, reason='kicked') + if roomname: + purge_list[user].append(roomname) + else: + purge_list[user].append(room) + time.sleep(self.config['sleep']) + except MNotFound: + pass + except Exception as e: + self.log.warning(e) + error_list[user] = [] + error_list[user].append(roomname or room) + + + results = "the following users were kicked:

{purge_list}

the following errors were \ + recorded:

{error_list}

".format(purge_list=purge_list, error_list=error_list) + await evt.respond(results, allow_html=True, edits=msg) + + # sync our database after we've made changes to room memberships + await self.do_sync() @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"]: - user = mxid - msg = await evt.respond("starting the ban...") - results_map = await self.ban_this_user(user, all_rooms=True) + 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) - results = "the following users were kicked and banned:

{ban_list}

the following errors were \ - recorded:

{error_list}

".format(ban_list=results_map['ban_list'], - error_list=results_map['error_list']) - await evt.respond(results, allow_html=True, edits=msg) + results = "the following users were kicked and banned:

{ban_list}

the following errors were \ + recorded:

{error_list}

".format(ban_list=results_map['ban_list'], + error_list=results_map['error_list']) + await evt.respond(results, allow_html=True, edits=msg) - # 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") + # sync our database after we've made changes to room memberships + await self.do_sync() @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"]: - user = mxid - msg = await evt.respond("starting the unban...") - roomlist = await self.get_space_roomlist() - # don't forget to kick from the space itself - roomlist.append(self.config["parent_room"]) - unban_list = {} - error_list = {} + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - unban_list[user] = [] - for room in roomlist: - try: - roomname = None - roomnamestate = await self.client.get_state_event(room, 'm.room.name') - roomname = roomnamestate['name'] + user = mxid + msg = await evt.respond("starting the unban...") + roomlist = await self.get_space_roomlist() + # don't forget to kick from the space itself + roomlist.append(self.config["parent_room"]) + unban_list = {} + error_list = {} - await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) - await self.client.unban_user(room, user, reason='unbanned') - if roomname: - unban_list[user].append(roomname) - else: - unban_list[user].append(room) - time.sleep(self.config['sleep']) - except MNotFound: - pass - except Exception as e: - self.log.warning(e) - error_list[user] = [] - error_list[user].append(roomname or room) + unban_list[user] = [] + for room in roomlist: + try: + roomname = None + roomnamestate = await self.client.get_state_event(room, 'm.room.name') + roomname = roomnamestate['name'] + + await self.client.get_state_event(room, EventType.ROOM_MEMBER, user) + await self.client.unban_user(room, user, reason='unbanned') + if roomname: + unban_list[user].append(roomname) + else: + unban_list[user].append(room) + time.sleep(self.config['sleep']) + except MNotFound: + pass + except Exception as e: + self.log.warning(e) + error_list[user] = [] + error_list[user].append(roomname or room) - results = "the following users were unbanned:

{unban_list}

the following errors were \ - recorded:

{error_list}

".format(unban_list=unban_list, error_list=error_list) - await evt.respond(results, allow_html=True, edits=msg) + results = "the following users were unbanned:

{unban_list}

the following errors were \ + recorded:

{error_list}

".format(unban_list=unban_list, error_list=error_list) + await evt.respond(results, allow_html=True, edits=msg) - # 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") + # sync our database after we've made changes to room memberships + await self.do_sync() @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 + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - # 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}") + 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: - await evt.reply("lol you don't have permission to do that") + 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}") async def create_room(self, roomname: str, evt: MessageEvent = None) -> None: """Create a new room and add it to the parent space. @@ -935,10 +1070,17 @@ class CommunityBot(Plugin): # Set bot PL higher than admin so we can kick old admins if needed pl_override = {"users": {self.client.mxid: 1000}} - for u in self.config['admins']: - pl_override["users"][u] = 100 - for u in self.config['moderators']: - pl_override["users"][u] = 50 + + # Get power levels from parent room + parent_power_levels = await self.client.get_state_event( + self.config["parent_room"], + EventType.ROOM_POWER_LEVELS + ) + + # Copy power levels from parent room for all users + for user, level in parent_power_levels.users.items(): + if user != self.client.mxid: # Skip bot's power level + pl_override["users"][user] = level if evt: mymsg = await evt.respond(f"creating {sanitized_name}, give me a minute...") @@ -996,156 +1138,212 @@ 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"]: - await self.create_room(roomname, evt) - else: - await evt.reply("you're not the boss of me!") + 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: + if force_encryption: + roomname = encrypted_flag_regex.sub('', roomname) + sanitized_name = re.sub(r"[^a-zA-Z0-9]", '', roomname).lower() + invitees = self.config['invitees'] + parent_room = self.config['parent_room'] + ## homeserver is derived from maubot's client instance since this is the user that will create the room + server = self.client.parse_user_id(self.client.mxid)[1] + # set bot PL higher than admin so we can kick old admins if needed + pl_override = {"users": {self.client.mxid: 1000}} + for u in self.config['admins']: + pl_override["users"][u] = 100 + for u in self.config['moderators']: + pl_override["users"][u] = 50 + pl_json = json.dumps(pl_override) + + mymsg = await evt.respond(f"creating {sanitized_name}, give me a minute...") + #self.log.info(mymsg) + room_id = await self.client.create_room(alias_localpart=sanitized_name, name=roomname, + invitees=invitees, power_level_override=pl_override) + time.sleep(self.config['sleep']) + + await evt.respond(f"updating room states...", edits=mymsg) + parent_event_content = json.dumps({'auto_join': False, 'suggested': False, 'via': [server]}) + child_event_content = json.dumps({'canonical': True, 'via': [server]}) + join_rules_content = json.dumps({'join_rule': 'restricted', 'allow': [{'type': 'm.room_membership', + 'room_id': parent_room}]}) + + await self.client.send_state_event(parent_room, 'm.space.child', parent_event_content, state_key=room_id) + time.sleep(self.config['sleep']) + await self.client.send_state_event(room_id, 'm.space.parent', child_event_content, state_key=parent_room) + time.sleep(self.config['sleep']) + await self.client.send_state_event(room_id, 'm.room.join_rules', join_rules_content, state_key="") + time.sleep(self.config['sleep']) + + if self.config["encrypt"] or force_encryption: + encryption_content = json.dumps({"algorithm": "m.megolm.v1.aes-sha2"}) + + await self.client.send_state_event(room_id, 'm.room.encryption', encryption_content, + state_key="") + await evt.respond(f"encrypting room...", edits=mymsg) + time.sleep(self.config['sleep']) + + await evt.respond(f"room created and updated, alias is #{sanitized_name}:{server}", edits=mymsg) + + + except Exception as e: + await evt.respond(f"i tried, but something went wrong: \"{e}\"", edits=mymsg) @community.subcommand("archive", help="archive a room") @command.argument("room", required=False) async def archive_room(self, evt: MessageEvent, room: str) -> None: await evt.mark_read() - if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]: - if not room: - room_id = evt.room_id - self.log.debug(f"DEBUG room we are archiving is {room_id}") - elif room and room.startswith('#'): - try: - self.log.debug(f"DEBUG trying to resolve alias {room}") - room_id = await self.client.resolve_room_alias(room) - room_id = room_id["room_id"] - self.log.debug(f"DEBUG room we are archiving is {room_id}") - except Exception as e: - evt.reply("i couldn't resolve that alias, sorry") - self.log.error(f"error resolving alias {room}: {e}") - return - elif room and room.startswith('!'): - room_id = room - self.log.debug(f"DEBUG room we are archiving is {room_id}") - else: - evt.reply("i don't recognize that room, sorry") - return + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - success = await self.do_archive_room(room_id, evt) - - # Only try to respond if we're not archiving the room we're in - if success and room_id != evt.room_id: - await evt.respond("Room has been archived.") + if not room: + room_id = evt.room_id + self.log.debug(f"DEBUG room we are archiving is {room_id}") + elif room and room.startswith('#'): + try: + self.log.debug(f"DEBUG trying to resolve alias {room}") + room_id = await self.client.resolve_room_alias(room) + room_id = room_id["room_id"] + self.log.debug(f"DEBUG room we are archiving is {room_id}") + except Exception as e: + evt.reply("i couldn't resolve that alias, sorry") + self.log.error(f"error resolving alias {room}: {e}") + return + elif room and room.startswith('!'): + room_id = room + self.log.debug(f"DEBUG room we are archiving is {room_id}") + else: + evt.reply("i don't recognize that room, sorry") + return + + success = await self.do_archive_room(room_id, evt) + + # Only try to respond if we're not archiving the room we're in + if success and room_id != evt.room_id: + await evt.respond("Room has been archived.") @community.subcommand("replaceroom", help="replace a room with a new one") @command.argument("room", required=False) async def replace_room(self, evt: MessageEvent, room: str) -> None: await evt.mark_read() - if evt.sender in self.config["admins"] or evt.sender in self.config["moderators"]: - if not room: - room = evt.room_id - # first we need to get relevant room state of the room we want to replace - # this includes the room name, alias, and join rules - if room.startswith('#'): - room_id = await self.client.resolve_room_alias(room) - room_id = room_id["room_id"] - else: - room_id = room + if not await self.user_permitted(evt.sender): + await evt.reply("You don't have permission to use this command") + return - # Check bot permissions in the old room - has_perms, error_msg, _ = await self.check_bot_permissions( - room_id, - evt, - ["state", "tombstone", "power_levels"] - ) - if not has_perms: - await evt.respond(f"Cannot replace room: {error_msg}") - return + if not room: + room = evt.room_id + # first we need to get relevant room state of the room we want to replace + # this includes the room name, alias, and join rules + if room.startswith('#'): + room_id = await self.client.resolve_room_alias(room) + room_id = room_id["room_id"] + else: + room_id = room - # Get the room name from the state event + # Check bot permissions in the old room + has_perms, error_msg, _ = await self.check_bot_permissions( + room_id, + evt, + ["state", "tombstone", "power_levels"] + ) + if not has_perms: + await evt.respond(f"Cannot replace room: {error_msg}") + return + + # Get the room name from the state event + try: + room_name_event = await self.client.get_state_event(room_id, EventType.ROOM_NAME) + room_name = room_name_event.name + except Exception as e: + self.log.warning(f"Failed to get room name: {e}") + #await evt.respond("Could not find room name in state events") + pass + + # get the room topic from the state event + try: + room_topic_event = await self.client.get_state_event(room_id, EventType.ROOM_TOPIC) + room_topic = room_topic_event.topic + except Exception as e: + self.log.warning(f"Failed to get room topic: {e}") + pass + + # Get list of aliases to transfer while removing them from the old room + aliases_to_transfer = await self.remove_room_aliases(room_id, evt) + + # Now we can start the process of replacing the room + # First we need to create the new room. this will create the initial alias, + # as well as bot defaults such as power levels, initial invitations, encryption, + # and space membership + new_room_id, new_room_alias = await self.create_room(room_name, evt) + if not new_room_id: + await evt.respond("Failed to create new room") + return + + # Check bot permissions in the new room + has_perms, error_msg, _ = await self.check_bot_permissions( + new_room_id, + evt, + ["state", "tombstone", "power_levels"] + ) + if not has_perms: + await evt.respond(f"Created new room but cannot complete replacement: {error_msg}") + return + + # Transfer the aliases to the new room + for alias in aliases_to_transfer: + localpart = alias.split(':')[0][1:] # Remove # and get localpart + server = alias.split(':')[1] try: - room_name_event = await self.client.get_state_event(room_id, EventType.ROOM_NAME) - room_name = room_name_event.name + await self.client.add_room_alias(new_room_id, localpart) + self.log.info(f"Successfully transferred alias {alias} to new room {new_room_id}") except Exception as e: - self.log.warning(f"Failed to get room name: {e}") - #await evt.respond("Could not find room name in state events") - pass - - # get the room topic from the state event - try: - room_topic_event = await self.client.get_state_event(room_id, EventType.ROOM_TOPIC) - room_topic = room_topic_event.topic - except Exception as e: - self.log.warning(f"Failed to get room topic: {e}") - pass - - # Get list of aliases to transfer while removing them from the old room - aliases_to_transfer = await self.remove_room_aliases(room_id, evt) - - # Now we can start the process of replacing the room - # First we need to create the new room. this will create the initial alias, - # as well as bot defaults such as power levels, initial invitations, encryption, - # and space membership - new_room_id, new_room_alias = await self.create_room(room_name, evt) - if not new_room_id: - await evt.respond("Failed to create new room") - return - - # Check bot permissions in the new room - has_perms, error_msg, _ = await self.check_bot_permissions( - new_room_id, - evt, - ["state", "tombstone", "power_levels"] - ) - if not has_perms: - await evt.respond(f"Created new room but cannot complete replacement: {error_msg}") - return - - # Transfer the aliases to the new room - for alias in aliases_to_transfer: - localpart = alias.split(':')[0][1:] # Remove # and get localpart - server = alias.split(':')[1] + # If transfer failed, try to create a modified alias + modified_alias = f"{localpart}NEW" try: - await self.client.add_room_alias(new_room_id, localpart) - self.log.info(f"Successfully transferred alias {alias} to new room {new_room_id}") - except Exception as e: - # If transfer failed, try to create a modified alias - modified_alias = f"{localpart}NEW" - try: - await self.client.add_room_alias(new_room_id, modified_alias) - self.log.info(f"Successfully transferred modified alias {modified_alias} to new room {new_room_id}") - except Exception as e2: - self.log.error(f"Failed to transfer modified alias {modified_alias}: {e2}") + await self.client.add_room_alias(new_room_id, modified_alias) + self.log.info(f"Successfully transferred modified alias {modified_alias} to new room {new_room_id}") + except Exception as e2: + self.log.error(f"Failed to transfer modified alias {modified_alias}: {e2}") - # Get the room avatar from the old room - try: - old_room_avatar = await self.client.get_state_event(room_id, EventType.ROOM_AVATAR) - if old_room_avatar and old_room_avatar.url: - # Set the same avatar in the new room - await self.client.send_state_event( - new_room_id, - EventType.ROOM_AVATAR, - {"url": old_room_avatar.url} - ) - self.log.info(f"Successfully copied room avatar to new room {new_room_id}") - except Exception as e: - self.log.error(f"Failed to copy room avatar to new room: {e}") - #await evt.respond(f"Failed to copy room avatar to new room: {e}") - - # Set the room topic in the new room - try: + # Get the room avatar from the old room + try: + old_room_avatar = await self.client.get_state_event(room_id, EventType.ROOM_AVATAR) + if old_room_avatar and old_room_avatar.url: + # Set the same avatar in the new room await self.client.send_state_event( new_room_id, - EventType.ROOM_TOPIC, - {"topic": room_topic} + EventType.ROOM_AVATAR, + {"url": old_room_avatar.url} ) - self.log.info(f"Successfully copied room topic to new room {new_room_id}") - except Exception as e: - self.log.error(f"Failed to copy room topic to new room: {e}") - #await evt.respond(f"Failed to copy room topic to new room: {e}") - + self.log.info(f"Successfully copied room avatar to new room {new_room_id}") + except Exception as e: + self.log.error(f"Failed to copy room avatar to new room: {e}") + #await evt.respond(f"Failed to copy room avatar to new room: {e}") - # Archive the old room with a pointer to the new room - success = await self.do_archive_room(room_id, evt, new_room_id) - if not success: - await evt.respond("Failed to archive old room, but new room has been created") + # Set the room topic in the new room + try: + await self.client.send_state_event( + new_room_id, + EventType.ROOM_TOPIC, + {"topic": room_topic} + ) + self.log.info(f"Successfully copied room topic to new room {new_room_id}") + except Exception as e: + self.log.error(f"Failed to copy room topic to new room: {e}") + #await evt.respond(f"Failed to copy room topic to new room: {e}") + + + # Archive the old room with a pointer to the new room + success = await self.do_archive_room(room_id, evt, new_room_id) + if not success: + await evt.respond("Failed to archive old room, but new room has been created") @community.subcommand("guests", help="generate a list of members in a room who are not members of the parent space") @command.argument("room", required=False) @@ -1202,68 +1400,68 @@ 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"]: - msg = await evt.respond("truing up power levels, this could take a minute...") - admins = self.config['admins'] - moderators = self.config['moderators'] - roomlist = await self.get_space_roomlist() - # don't forget to include the space itself - roomlist.append(self.config["parent_room"]) - success_list = [] - error_list = [] - adminpl = 100 - modpl = 50 - defaultpl = 0 + if not await self.user_permitted(evt.sender, min_level=100): + await evt.reply("You don't have permission to use this command") + return - for room in roomlist: - # need to get and evaluate the current state that contains powerlevels first - current_pl = await self.client.get_state_event(room, 'm.room.power_levels') - users = current_pl['users'].serialize() - updated_user_map = dict(users) - try: - roomname = None - roomnamestate = await self.client.get_state_event(room, 'm.room.name') - roomname = roomnamestate['name'] - except Exception as e: - self.log.warning(e) + msg = await evt.respond("truing up power levels, this could take a minute...") + admins = self.config['admins'] + moderators = self.config['moderators'] + roomlist = await self.get_space_roomlist() + # don't forget to include the space itself + roomlist.append(self.config["parent_room"]) + success_list = [] + error_list = [] + adminpl = 100 + modpl = 50 + defaultpl = 0 - # update our powerlevel map values - for user in admins: - updated_user_map[user] = adminpl - for user in moderators: - updated_user_map[user] = modpl + for room in roomlist: + # need to get and evaluate the current state that contains powerlevels first + current_pl = await self.client.get_state_event(room, 'm.room.power_levels') + users = current_pl['users'].serialize() + updated_user_map = dict(users) + try: + roomname = None + roomnamestate = await self.client.get_state_event(room, 'm.room.name') + roomname = roomnamestate['name'] + except Exception as e: + self.log.warning(e) - # revoke values for people no longer in the config - for user in users.keys(): - if ( user not in admins and - user not in moderators and - updated_user_map[user] > defaultpl and - user != self.client.mxid ): - del updated_user_map[user] + # update our powerlevel map values + for user in admins: + updated_user_map[user] = adminpl + for user in moderators: + updated_user_map[user] = modpl + + # revoke values for people no longer in the config + for user in users.keys(): + if ( user not in admins and + user not in moderators and + updated_user_map[user] > defaultpl and + user != self.client.mxid ): + del updated_user_map[user] - # and send the new state event back to the room - new_pl = current_pl - new_pl['users'] = updated_user_map - try: - #self.log.debug(f"DEBUG sending finalized PL map to room {room}: {updated_user_map}") - await self.client.send_state_event(room, 'm.room.power_levels', new_pl) - success_list.append(roomname or room) - except Exception as e: - self.log.warning(e) - error_list.append(roomname or room) + # and send the new state event back to the room + new_pl = current_pl + new_pl['users'] = updated_user_map + try: + #self.log.debug(f"DEBUG sending finalized PL map to room {room}: {updated_user_map}") + await self.client.send_state_event(room, 'm.room.power_levels', new_pl) + success_list.append(roomname or room) + except Exception as e: + self.log.warning(e) + error_list.append(roomname or room) - time.sleep(self.config['sleep']) + time.sleep(self.config['sleep']) - results = "the following rooms were updated:

{success_list}

the following errors were \ - recorded:

{error_list}

".format(success_list=success_list, error_list=error_list) - await evt.respond(results, allow_html=True, edits=msg) + results = "the following rooms were updated:

{success_list}

the following errors were \ + recorded:

{error_list}

".format(success_list=success_list, error_list=error_list) + await evt.respond(results, allow_html=True, edits=msg) - # 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") + # sync our database after we've made changes to room memberships + await self.do_sync()