- Major back-end changes - Moved most non-sensitive values to dynamic configurations module - Removed references to old variables naming scheme for certain settings - Essentially, most settings are now capable of being dynamically assigned, instead of fully static - Complete rework of the wrapper and compose infrastructure to allow dynamic data changes - New command: `/shaiadmin` - Admin-only (and approved users/roles) have access - `/shaiadmin set [setting] [value]` - Applies a new setting - Settings must be properly formatted, and invalid ones are rejected - `/shaiadmin unset [setting]` - Unsets/resets a setting to default - `/shaiadmin settings download` - Offers the current settings file for download. Useful for bulk editing. JSON formatted - `/shaiadmin settings upload [file].json` - Allows the uploading of a new settings file - This file is verified, tested, and processed before being applied
169 lines
6.8 KiB
Python
169 lines
6.8 KiB
Python
# modules/admin/shaiadmin.py
|
||
import io
|
||
import json
|
||
from typing import Any, Dict, List
|
||
|
||
import discord
|
||
from discord import app_commands
|
||
from discord.ext import commands
|
||
|
||
from modules.common.settings import (
|
||
cfg, SETTINGS_SCHEMA, settings_path, settings_get_all,
|
||
settings_set, settings_reset, settings_import_bulk, ValidationError,
|
||
)
|
||
from mod_perms import is_moderator_member # keep if you want mods as managers
|
||
|
||
def _user_has_role_ids(member: discord.Member, role_ids: List[int]) -> bool:
|
||
if not isinstance(member, discord.Member) or not role_ids:
|
||
return False
|
||
rset = set(role_ids)
|
||
return any(r.id in rset for r in member.roles)
|
||
|
||
async def _is_owner(bot: commands.Bot, user: discord.abc.User) -> bool:
|
||
try:
|
||
return await bot.is_owner(user)
|
||
except Exception:
|
||
return False
|
||
|
||
def _get_admin_lists(bot: commands.Bot) -> Dict[str, List[int]]:
|
||
r = cfg(bot)
|
||
users, roles = [], []
|
||
try:
|
||
users = json.loads(r.get("admin_user_ids", "[]"))
|
||
except Exception:
|
||
users = []
|
||
try:
|
||
roles = json.loads(r.get("admin_role_ids", "[]"))
|
||
except Exception:
|
||
roles = []
|
||
return {"users": users, "roles": roles}
|
||
|
||
async def _check_admin(inter: discord.Interaction) -> bool:
|
||
bot: commands.Bot = inter.client # type: ignore
|
||
user = inter.user
|
||
if await _is_owner(bot, user):
|
||
return True
|
||
if isinstance(user, discord.Member):
|
||
lists = _get_admin_lists(bot)
|
||
if user.id in set(lists["users"]):
|
||
return True
|
||
if _user_has_role_ids(user, lists["roles"]):
|
||
return True
|
||
if is_moderator_member(user, bot): # optional; remove if not desired
|
||
return True
|
||
if not inter.response.is_done():
|
||
await inter.response.send_message("You don’t have permission to use `/shaiadmin`.", ephemeral=True)
|
||
else:
|
||
await inter.followup.send("You don’t have permission to use `/shaiadmin`.", ephemeral=True)
|
||
return False
|
||
|
||
|
||
class ShaiAdminCog(commands.Cog):
|
||
"""Runtime settings administration (file-backed)."""
|
||
|
||
def __init__(self, bot: commands.Bot):
|
||
self.bot = bot
|
||
|
||
# Guild-only group; prefix description with [ADMIN]
|
||
shaiadmin = app_commands.Group(
|
||
name="shaiadmin",
|
||
description="[ADMIN] Owner/approved-only settings manager.",
|
||
guild_only=True,
|
||
)
|
||
|
||
# ---- bound coroutine for autocomplete ----
|
||
async def ac_setting_keys(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]:
|
||
cur = (current or "").lower()
|
||
keys = [k for k in sorted(SETTINGS_SCHEMA.keys()) if cur in k]
|
||
return [app_commands.Choice(name=k, value=k) for k in keys[:25]]
|
||
|
||
# /shaiadmin set
|
||
@shaiadmin.command(name="set", description="[ADMIN] Set a setting (validated, persisted, applied).")
|
||
@app_commands.describe(setting_name="Which setting to change", value="New value (type depends on setting)")
|
||
@app_commands.autocomplete(setting_name=ac_setting_keys)
|
||
async def set_value(self, inter: discord.Interaction, setting_name: str, value: str):
|
||
if not await _check_admin(inter):
|
||
return
|
||
await inter.response.defer(ephemeral=True, thinking=True)
|
||
setting_name = setting_name.lower().strip()
|
||
try:
|
||
changed = settings_set(setting_name, value)
|
||
await inter.followup.send(
|
||
f"✅ `{setting_name}` updated and applied." if changed else "ℹ️ No change.",
|
||
ephemeral=True,
|
||
)
|
||
except ValidationError as ve:
|
||
await inter.followup.send(f"❌ {ve}", ephemeral=True)
|
||
except Exception as e:
|
||
await inter.followup.send(f"❌ Failed to set `{setting_name}`: {e!r}", ephemeral=True)
|
||
|
||
# /shaiadmin unset
|
||
@shaiadmin.command(name="unset", description="[ADMIN] Reset/unset a setting to its default.")
|
||
@app_commands.describe(setting_name="Which setting to reset")
|
||
@app_commands.autocomplete(setting_name=ac_setting_keys)
|
||
async def unset_value(self, inter: discord.Interaction, setting_name: str):
|
||
if not await _check_admin(inter):
|
||
return
|
||
await inter.response.defer(ephemeral=True, thinking=True)
|
||
setting_name = setting_name.lower().strip()
|
||
try:
|
||
settings_reset(setting_name)
|
||
await inter.followup.send(f"✅ `{setting_name}` reset to default and applied.", ephemeral=True)
|
||
except ValidationError as ve:
|
||
await inter.followup.send(f"❌ {ve}", ephemeral=True)
|
||
except Exception as e:
|
||
await inter.followup.send(f"❌ Failed to reset `{setting_name}`: {e!r}", ephemeral=True)
|
||
|
||
# /shaiadmin settings (download/upload)
|
||
settings = app_commands.Group(
|
||
name="settings",
|
||
description="[ADMIN] Download or upload the full settings JSON.",
|
||
parent=shaiadmin,
|
||
guild_only=True,
|
||
)
|
||
|
||
@settings.command(name="download", description="[ADMIN] Download the current settings.json")
|
||
async def download(self, inter: discord.Interaction):
|
||
if not await _check_admin(inter):
|
||
return
|
||
await inter.response.defer(ephemeral=True, thinking=True)
|
||
data = settings_get_all()
|
||
buf = io.BytesIO(json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8"))
|
||
buf.seek(0)
|
||
await inter.followup.send(
|
||
content=f"📦 Current settings from `{settings_path()}`",
|
||
file=discord.File(buf, filename="settings.json"),
|
||
ephemeral=True,
|
||
)
|
||
|
||
@settings.command(name="upload", description="[ADMIN] Upload and apply a settings.json")
|
||
@app_commands.describe(file="A JSON file exported by /shaiadmin settings download (or matching the schema).")
|
||
async def upload(self, inter: discord.Interaction, file: discord.Attachment):
|
||
if not await _check_admin(inter):
|
||
return
|
||
await inter.response.defer(ephemeral=True, thinking=True)
|
||
if not file or not file.filename.lower().endswith(".json"):
|
||
await inter.followup.send("Please attach a `.json` file.", ephemeral=True)
|
||
return
|
||
try:
|
||
raw = await file.read()
|
||
obj = json.loads(raw.decode("utf-8"))
|
||
except Exception:
|
||
await inter.followup.send("❌ Invalid JSON file.", ephemeral=True)
|
||
return
|
||
|
||
try:
|
||
changed_keys = settings_import_bulk(obj)
|
||
await inter.followup.send(
|
||
f"✅ Uploaded and applied `{len(changed_keys)}` keys: {', '.join(sorted(changed_keys))}."
|
||
if changed_keys else "ℹ️ No changes detected.",
|
||
ephemeral=True,
|
||
)
|
||
except ValidationError as ve:
|
||
await inter.followup.send(f"❌ {ve}", ephemeral=True)
|
||
except Exception as e:
|
||
await inter.followup.send(f"❌ Upload failed: {e!r}", ephemeral=True)
|
||
|
||
async def setup(bot: commands.Bot):
|
||
await bot.add_cog(ShaiAdminCog(bot))
|