Commands now show as 'wh create', 'wh staff add', 'wh set relaymode' instead of 'wh-create', 'wh-staff add', 'wh-set relaymode'. - Added commands/_base.py with root 'wh' group and all 20 subgroup stubs - All 13 command mixins inherit from WormholeBase - Decorators changed: @commands.hybrid_command(name='wh-X') → @WormholeBase.wh.command(name='X') - Subgroup commands: @wh_X.command() → @WormholeBase.wh_X.command() - Removed duplicate 'wh' group from core.py (now in _base.py) - Fixed alias: 'wh-color' → 'color' for colour command
190 lines
8.3 KiB
Python
190 lines
8.3 KiB
Python
"""Staff management commands — assign roles, list staff, transfer ownership."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import discord
|
|
from redbot.core import commands
|
|
|
|
from ..models.permissions import (
|
|
Role,
|
|
can_assign,
|
|
get_role,
|
|
has_role,
|
|
list_staff,
|
|
requires_role,
|
|
role_from_str,
|
|
role_name,
|
|
set_staff_role,
|
|
remove_staff,
|
|
)
|
|
from ..utils import ok_embed, err_embed, info_embed, COLOUR_INFO
|
|
from ._base import WormholeBase
|
|
|
|
|
|
class StaffCommands(WormholeBase):
|
|
"""Mixin — staff management (per-network hierarchy)."""
|
|
|
|
@WormholeBase.wh_staff.command(name="add")
|
|
async def wh_staff_add(self, ctx: commands.Context, name: str, user: discord.User, role_str: str = "moderator") -> None:
|
|
"""Assign a staff role to a user.
|
|
|
|
Roles: ``admin``, ``moderator``, ``helper``
|
|
"""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
|
|
target_role = role_from_str(role_str)
|
|
if target_role is None or target_role <= Role.MEMBER or target_role >= Role.OWNER:
|
|
return await ctx.send(embed=err_embed("Valid roles: `admin`, `moderator`, `helper`."))
|
|
|
|
actor_role = get_role(nd, ctx.author.id)
|
|
is_bot_owner = await self.bot.is_owner(ctx.author)
|
|
if not is_bot_owner and not can_assign(actor_role, target_role):
|
|
return await ctx.send(embed=err_embed(
|
|
f"You need a higher role than **{role_name(target_role)}** to assign it.\n"
|
|
f"Your role: **{role_name(actor_role)}**"
|
|
))
|
|
|
|
async with self.config.networks() as ns:
|
|
set_staff_role(ns[name], user.id, target_role)
|
|
await ctx.send(embed=ok_embed(
|
|
f"{user.mention} is now **{role_name(target_role)}** in `{name}`."
|
|
))
|
|
await self._audit(name, "staff_add", str(ctx.author), str(user), f"role={role_name(target_role)}")
|
|
|
|
@WormholeBase.wh_staff.command(name="remove", aliases=["rm"])
|
|
async def wh_staff_rm(self, ctx: commands.Context, name: str, user: discord.User) -> None:
|
|
"""Remove a user from network staff."""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
|
|
target_role = get_role(nd, user.id)
|
|
if target_role <= Role.MEMBER:
|
|
return await ctx.send(embed=err_embed(f"{user} isn't staff in `{name}`."))
|
|
|
|
actor_role = get_role(nd, ctx.author.id)
|
|
is_bot_owner = await self.bot.is_owner(ctx.author)
|
|
if not is_bot_owner and not can_assign(actor_role, target_role):
|
|
return await ctx.send(embed=err_embed(
|
|
f"You can't remove a **{role_name(target_role)}** — "
|
|
f"your role: **{role_name(actor_role)}**"
|
|
))
|
|
|
|
async with self.config.networks() as ns:
|
|
remove_staff(ns[name], user.id)
|
|
await ctx.send(embed=ok_embed(f"{user.mention} removed from staff in `{name}`."))
|
|
await self._audit(name, "staff_rm", str(ctx.author), str(user))
|
|
|
|
@WormholeBase.wh_staff.command(name="promote")
|
|
async def wh_staff_promote(self, ctx: commands.Context, name: str, user: discord.User) -> None:
|
|
"""Promote a staff member one tier up."""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
|
|
current = get_role(nd, user.id)
|
|
if current >= Role.ADMIN:
|
|
return await ctx.send(embed=err_embed(f"{user} is already **{role_name(current)}** — can't promote further."))
|
|
new_role = Role(current + 1)
|
|
if new_role >= Role.OWNER:
|
|
return await ctx.send(embed=err_embed("Can't promote to Owner. Use `wh transfer` instead."))
|
|
|
|
actor_role = get_role(nd, ctx.author.id)
|
|
is_bot_owner = await self.bot.is_owner(ctx.author)
|
|
if not is_bot_owner and not can_assign(actor_role, new_role):
|
|
return await ctx.send(embed=err_embed(
|
|
f"You can't assign **{role_name(new_role)}** — "
|
|
f"your role: **{role_name(actor_role)}**"
|
|
))
|
|
|
|
async with self.config.networks() as ns:
|
|
set_staff_role(ns[name], user.id, new_role)
|
|
await ctx.send(embed=ok_embed(
|
|
f"{user.mention} promoted: **{role_name(current)}** → **{role_name(new_role)}** in `{name}`."
|
|
))
|
|
await self._audit(name, "promote", str(ctx.author), str(user), f"{role_name(current)} → {role_name(new_role)}")
|
|
|
|
@WormholeBase.wh_staff.command(name="demote")
|
|
async def wh_staff_demote(self, ctx: commands.Context, name: str, user: discord.User) -> None:
|
|
"""Demote a staff member one tier down."""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
|
|
current = get_role(nd, user.id)
|
|
if current <= Role.MEMBER:
|
|
return await ctx.send(embed=err_embed(f"{user} isn't staff in `{name}`."))
|
|
if current >= Role.OWNER:
|
|
return await ctx.send(embed=err_embed("Can't demote the Owner. Use `wh transfer` first."))
|
|
|
|
new_role = Role(current - 1)
|
|
|
|
actor_role = get_role(nd, ctx.author.id)
|
|
is_bot_owner = await self.bot.is_owner(ctx.author)
|
|
if not is_bot_owner and not can_assign(actor_role, current):
|
|
return await ctx.send(embed=err_embed(
|
|
f"You can't demote a **{role_name(current)}** — "
|
|
f"your role: **{role_name(actor_role)}**"
|
|
))
|
|
|
|
async with self.config.networks() as ns:
|
|
if new_role <= Role.MEMBER:
|
|
remove_staff(ns[name], user.id)
|
|
else:
|
|
set_staff_role(ns[name], user.id, new_role)
|
|
label = role_name(new_role) if new_role > Role.MEMBER else "Member (removed from staff)"
|
|
await ctx.send(embed=ok_embed(
|
|
f"{user.mention} demoted: **{role_name(current)}** → **{label}** in `{name}`."
|
|
))
|
|
await self._audit(name, "demote", str(ctx.author), str(user), f"{role_name(current)} → {label}")
|
|
|
|
@WormholeBase.wh_staff.command(name="list", aliases=["ls"])
|
|
async def wh_staff_ls(self, ctx: commands.Context, name: str) -> None:
|
|
"""List all staff in a network with their roles."""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
|
|
staff = list_staff(nd)
|
|
if not staff:
|
|
return await ctx.send(embed=info_embed(f"No staff in `{name}`."))
|
|
|
|
lines = []
|
|
for uid, role in sorted(staff.items(), key=lambda x: -x[1]):
|
|
user = self.bot.get_user(uid)
|
|
display = str(user) if user else f"Unknown ({uid})"
|
|
emoji = {Role.OWNER: "👑", Role.ADMIN: "🛡️", Role.MODERATOR: "⚔️", Role.HELPER: "🤝"}.get(role, "")
|
|
lines.append(f"{emoji} **{role_name(role)}** — {display}")
|
|
|
|
em = discord.Embed(
|
|
title=f"👥 Staff — {name}",
|
|
description="\n".join(lines),
|
|
colour=COLOUR_INFO,
|
|
)
|
|
await ctx.send(embed=em)
|
|
|
|
@WormholeBase.wh.command(name="transfer")
|
|
async def wh_transfer(self, ctx: commands.Context, name: str, new_owner: discord.User) -> None:
|
|
"""Transfer network ownership (owner or bot owner only)."""
|
|
nd = await self._net(name)
|
|
if not nd:
|
|
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
|
if not has_role(nd, ctx.author.id, Role.OWNER) and not await self.bot.is_owner(ctx.author):
|
|
return await ctx.send(embed=err_embed("Only the network owner or bot owner can transfer ownership."))
|
|
|
|
old_owner = nd["owner_id"]
|
|
async with self.config.networks() as ns:
|
|
ns[name]["owner_id"] = new_owner.id
|
|
# Demote old owner to Admin if they're not the bot owner
|
|
if old_owner != new_owner.id:
|
|
set_staff_role(ns[name], old_owner, Role.ADMIN)
|
|
# Remove new owner from staff dict (they're owner now)
|
|
remove_staff(ns[name], new_owner.id)
|
|
|
|
await ctx.send(embed=ok_embed(
|
|
f"Ownership of `{name}` transferred to {new_owner.mention}.\n"
|
|
f"Previous owner demoted to Admin."
|
|
))
|
|
await self._audit(name, "transfer", str(ctx.author), str(new_owner))
|