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"]),
|
("🎲 Random", ["flip", "roll", "random", "champion", "agent"]),
|
||||||
("🖥️ Server", ["minecraft", "ping", "health"]),
|
("🖥️ Server", ["minecraft", "ping", "health"]),
|
||||||
("🔧 Management (PL50+)", ["mkroom", "roominfo", "topic", "invite", "setpl"]),
|
("🔧 Management (PL50+)", ["mkroom", "roominfo", "topic", "invite", "inviteall", "setpl"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
plain_lines = ["LotusBot Commands"]
|
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)
|
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+)")
|
@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):
|
async def cmd_mkroom(client: AsyncClient, room_id: str, sender: str, args: str):
|
||||||
if not is_elevated(client, room_id, sender):
|
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.")
|
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):
|
async def cmd_setpl(client: AsyncClient, room_id: str, sender: str, args: str):
|
||||||
if not is_elevated(client, room_id, sender):
|
if not is_elevated(client, room_id, sender):
|
||||||
await send_text(client, room_id, "⛔ This command requires power level 50 or higher.")
|
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}).")
|
f"⛔ You can't set a power level higher than your own ({sender_level}).")
|
||||||
return
|
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("@")
|
name = target.split(":")[0].lstrip("@")
|
||||||
await send_text(client, room_id, f"✅ {name}'s power level set to {new_level}.")
|
await send_text(client, room_id,
|
||||||
except Exception as e:
|
f"⏳ Applying PL{new_level} to {name} across all Space rooms…")
|
||||||
logger.error("setpl error: %s", e, exc_info=True)
|
|
||||||
await send_text(client, room_id, "❌ Failed to update power level.")
|
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