feat: setpl and inviteall apply space-wide with smart filtering
!setpl now iterates every Space room via the hierarchy API and updates the power_levels state event in each. Setting a user to the room's users_default cleans up the explicit entry rather than leaving a stale PL0. Rooms where the bot lacks permission are counted and reported but don't block the rest. !inviteall skips rooms with join_rule=invite (Management, Cool Kids, Spam and Stuff) — only public/restricted rooms get the invite. Also skips rooms where the target is already a member. _get_space_room_ids() fetches the Space child list via the v1 hierarchy API with pagination support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+116
-19
@@ -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'<font color="#22c55e"><strong>✅ Invites sent</strong></font><br>'
|
||||
f'<strong>{name}</strong> invited to <strong>{len(sent)}</strong> public room(s)'
|
||||
+ (f'<br><em>ℹ️ {len(skipped)} private/invite-only rooms skipped</em>' 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.")
|
||||
return
|
||||
|
||||
current_pl.setdefault("users", {})[target] = new_level
|
||||
|
||||
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.")
|
||||
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
|
||||
|
||||
updated: list[str] = []
|
||||
skipped: list[str] = []
|
||||
|
||||
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'<font color="#22c55e"><strong>✅ Power level updated</strong></font><br>'
|
||||
f'<strong>{name}</strong> → PL{new_level} in <strong>{len(updated)}</strong> room(s)'
|
||||
+ (f'<br><em>⚠️ {len(skipped)} room(s) skipped (bot lacks permission)</em>' if skipped else ""),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user