diff --git a/README.md b/README.md index 3a45f9b..fe95fd4 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,24 @@ need to rejoin all rooms themselves or be re-invited. use the `guests` subcommand to see who is in a room but NOT a member of the parent space (invited guests) e.g. `!community guests #myroom:alias.here`. +## admin/moderator management + +set consistent power levels across all your rooms for your community administrators! the config defines a list of both +admins and moderators (admins have a Power Level of 100, mods have PL50). running the setpower subcommand (i.e. +`!community setpower`) will roll through all rooms in the space (including the space itself) and attempt to true-up user +permissions to match. if you are running legacy rooms not managed by the bot, and the bot does not have permission to +send power-level state events to the room, it will return a list for you to handle manually. users who have a PL greater +than 0 and are not listed as either an admin or moderator will be removed from the permission list, effectively +returning their power to whatever the room default is (usually 0). + ## room creation use the `createroom` subcommand to create a new room according to your preferences, and join it into the parent space. will attempt to sanitize the room name and assign a room alias automatically. the bot user will be assigned very high -power level (1000) and set an admin power level (100) to plugin administrators. this ensures that the bot is still able -to manage room admins. the bot will also invite other users to these new rooms as configured. +power level (1000) and set an admin power level (100) to plugin administrators, 50 to moderators. this ensures that the +bot is still able to manage room admins. the bot will also invite other users to these new rooms as configured. + +rooms created by the bot will have join restriction limited to members of the space. ## get room ID diff --git a/base-config.yaml b/base-config.yaml index 3f80b32..41de195 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -19,12 +19,16 @@ track_messages: True # will update the user's last-active date when they add a reaction to a message track_reactions: True -# list of users who can use administrative commands, these users will also be made room admins (PL100) -# in new rooms created with the create room commands +# list of users who can use administrative commands. these users will also be made room admins (PL100) admins: - '@user1:server.tld' - '@user2:server.tld' +# list of users who should be considered community moderators. these users will be made room mods (PL50) +moderators: + - '@user3:server.tld' + - '@user4:server.tld' + # list of users who should be invited to new rooms immediately (other bots perhaps) invitees: - "@mybot:server.tld" diff --git a/community/bot.py b/community/bot.py index 84cfb15..2f6566f 100644 --- a/community/bot.py +++ b/community/bot.py @@ -8,7 +8,7 @@ import re from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType, MediaMessageEventContent, ReactionEvent, RedactionEvent, RoomID, - RoomAlias) + RoomAlias, PowerLevelStateEventContent) from mautrix.errors import MNotFound from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper from maubot import Plugin, MessageEvent @@ -22,6 +22,7 @@ from .db import upgrade_table class Config(BaseProxyConfig): def do_update(self, helper: ConfigUpdateHelper) -> None: helper.copy("admins") + helper.copy("moderators") helper.copy("parent_room") helper.copy("track_messages") helper.copy("track_reactions") @@ -421,6 +422,8 @@ class CommunityBot(Plugin): 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...") @@ -510,6 +513,74 @@ class CommunityBot(Plugin): except Exception as e: await evt.respond(f"something went wrong: {e}") + @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 + + 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) + + # 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) + + time.sleep(0.5) + + results = "the following rooms were updated:
{success_list}
{error_list}