shaiwatcher/modules/admin/shaiadmin.py
Franz Rolfsvaag 23e122c08a 0.5.1.1.a1
- 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
2025-08-24 15:27:10 +02:00

169 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 dont have permission to use `/shaiadmin`.", ephemeral=True)
else:
await inter.followup.send("You dont 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))