diff --git a/matrixbot/commands.py b/matrixbot/commands.py index 5c01180..c6ff0fa 100644 --- a/matrixbot/commands.py +++ b/matrixbot/commands.py @@ -139,7 +139,7 @@ async def cmd_help(client: AsyncClient, room_id: str, sender: str, args: str): ]), ("🎲 Random", ["flip", "roll", "random", "champion", "agent"]), ("πŸ–₯️ Server", ["minecraft", "ping", "health"]), - ("πŸ”§ Management (PL50+)", ["mkroom", "roominfo", "topic", "invite", "setpl"]), + ("πŸ”§ Management (PL50+)", ["mkroom", "roominfo", "topic", "invite", "inviteall", "setpl"]), ] plain_lines = ["LotusBot Commands"] @@ -3731,6 +3731,31 @@ async def _put_state(client: AsyncClient, room_id: str, event_type: str, content return await _mx(client, "put", path, content) +async def _get_space_room_ids(client: AsyncClient) -> list[str]: + """Return all room IDs that are direct children of the Lotus Guild Space.""" + rooms: list[str] = [] + next_batch: str | None = None + while True: + path = ( + f"/_matrix/client/v1/rooms/{_url_quote(_MGMT_SPACE_ID)}/hierarchy" + f"?limit=50&max_depth=1" + + (f"&from={_url_quote(next_batch)}" if next_batch else "") + ) + try: + data = await _mx(client, "get", path) + except Exception as e: + logger.error("space hierarchy fetch error: %s", e) + break + for room in data.get("rooms", []): + rid = room.get("room_id", "") + if rid and rid != _MGMT_SPACE_ID: + rooms.append(rid) + next_batch = data.get("next_batch") + if not next_batch: + break + return rooms + + @command("mkroom", "Create a new room using #general as the template (PL50+)") async def cmd_mkroom(client: AsyncClient, room_id: str, sender: str, args: str): if not is_elevated(client, room_id, sender): @@ -3896,7 +3921,59 @@ async def cmd_invite(client: AsyncClient, room_id: str, sender: str, args: str): await send_text(client, room_id, "❌ Failed to send invite.") -@command("setpl", "Set a user's power level (PL50+) β€” !setpl @user <0-100>") +@command("inviteall", "Invite a user to all public/restricted Space rooms β€” skips private channels (PL50+)") +async def cmd_inviteall(client: AsyncClient, room_id: str, sender: str, args: str): + if not is_elevated(client, room_id, sender): + await send_text(client, room_id, "β›” This command requires power level 50 or higher.") + return + + target = args.strip() + if not target or not target.startswith("@"): + await send_text(client, room_id, f"Usage: {BOT_PREFIX}inviteall @user:server") + return + + name = target.split(":")[0].lstrip("@") + await send_text(client, room_id, f"⏳ Inviting {name} to public/restricted Space rooms…") + + space_rooms = await _get_space_room_ids(client) + if not space_rooms: + await send_text(client, room_id, "❌ Couldn't fetch Space room list.") + return + + sent: list[str] = [] + skipped: list[str] = [] + + for target_room_id in space_rooms: + try: + r = client.rooms.get(target_room_id) + # Skip invite-only rooms β€” those are intentionally private + if r and getattr(r, "join_rule", "invite") == "invite": + skipped.append(target_room_id) + continue + # Skip rooms the user is already in + if r and target in r.users: + skipped.append(target_room_id) + continue + result = await _mx(client, "post", + f"/_matrix/client/v3/rooms/{_url_quote(target_room_id)}/invite", + {"user_id": target}) + if "errcode" in result: + skipped.append(target_room_id) + else: + sent.append(target_room_id) + except Exception: + skipped.append(target_room_id) + + skip_note = f" ({len(skipped)} private/inaccessible rooms skipped)" if skipped else "" + await send_html(client, room_id, + f"βœ… Sent {len(sent)} invite(s) to {name}.{skip_note}", + f'βœ… Invites sent
' + f'{name} invited to {len(sent)} public room(s)' + + (f'
ℹ️ {len(skipped)} private/invite-only rooms skipped' if skipped else ""), + ) + + +@command("setpl", "Set a user's power level across all Space rooms (PL50+) β€” !setpl @user <0-100>") async def cmd_setpl(client: AsyncClient, room_id: str, sender: str, args: str): if not is_elevated(client, room_id, sender): await send_text(client, room_id, "β›” This command requires power level 50 or higher.") @@ -3925,21 +4002,41 @@ async def cmd_setpl(client: AsyncClient, room_id: str, sender: str, args: str): f"β›” You can't set a power level higher than your own ({sender_level}).") return - # Fetch current power_levels state and patch it - current_pl = await _get_state(client, room_id, "m.room.power_levels") - if not current_pl: - await send_text(client, room_id, "❌ Couldn't fetch current power levels.") + name = target.split(":")[0].lstrip("@") + await send_text(client, room_id, + f"⏳ Applying PL{new_level} to {name} across all Space rooms…") + + space_rooms = await _get_space_room_ids(client) + if not space_rooms: + await send_text(client, room_id, "❌ Couldn't fetch Space room list.") return - current_pl.setdefault("users", {})[target] = new_level + updated: list[str] = [] + skipped: list[str] = [] - try: - result = await _put_state(client, room_id, "m.room.power_levels", current_pl) - if "errcode" in result: - await send_text(client, room_id, f"❌ Failed: {result.get('error', result['errcode'])}") - return - name = target.split(":")[0].lstrip("@") - await send_text(client, room_id, f"βœ… {name}'s power level set to {new_level}.") - except Exception as e: - logger.error("setpl error: %s", e, exc_info=True) - await send_text(client, room_id, "❌ Failed to update power level.") + for target_room_id in space_rooms: + current_pl = await _get_state(client, target_room_id, "m.room.power_levels") + if not current_pl: + skipped.append(target_room_id) + continue + users = current_pl.setdefault("users", {}) + if new_level == current_pl.get("users_default", 0): + users.pop(target, None) # remove explicit entry β€” they'll use the room default + else: + users[target] = new_level + try: + result = await _put_state(client, target_room_id, "m.room.power_levels", current_pl) + if "errcode" in result: + skipped.append(target_room_id) + else: + updated.append(target_room_id) + except Exception: + skipped.append(target_room_id) + + skip_note = f" ({len(skipped)} skipped β€” bot lacks permission)" if skipped else "" + await send_html(client, room_id, + f"βœ… {name} is now PL{new_level} in {len(updated)} Space room(s).{skip_note}", + f'βœ… Power level updated
' + f'{name} β†’ PL{new_level} in {len(updated)} room(s)' + + (f'
⚠️ {len(skipped)} room(s) skipped (bot lacks permission)' if skipped else ""), + )