shaiwatcher/bot.py
Franz Rolfsvaag 40ef32c530 0.3.9.6.a3
Cogified the restart command `/bot_restart {reason}` -> `/power restart {reason}` for future enhancements
2025-08-11 02:24:23 +02:00

234 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

import os, signal, asyncio, pathlib
import discord
from discord.ext import commands
from dotenv import load_dotenv
from data_manager import DataManager
from modules.common.settings import cfg as cfg_helper
from modules.common.boot_notice import post_boot_notice
# Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.6.a3"
# ---------- Env loading ----------
load_dotenv()
def _get_env(name: str, default: str = "") -> str:
v = os.getenv(name, "")
return (v or "").strip().strip('"').strip("'") or default
TOKEN = _get_env("DISCORD_TOKEN")
DATA_FILE = _get_env("SHAI_DATA") or _get_env("SHAI_DATA_FILE") or "/data/data.json"
print("[Config] DISCORD_TOKEN set:", bool(TOKEN))
print("[Config] DATA_FILE:", DATA_FILE)
# ---------- Discord intents ----------
intents = discord.Intents.default()
intents.guilds = True
intents.members = True
intents.message_content = True
intents.reactions = True
intents.emojis_and_stickers = True
intents.voice_states = True
# ---------- Bot + DataManager ----------
if not TOKEN:
print("[Config] WARNING: DISCORD_TOKEN is empty. The bot will fail to log in.")
bot = commands.Bot(command_prefix="!", intents=intents)
# Ensure data path exists and is seeded
os.makedirs(os.path.dirname(DATA_FILE) or ".", exist_ok=True)
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, "w", encoding="utf-8") as f:
f.write("{}")
bot.data_manager = DataManager(DATA_FILE)
# ---------- Self-check: resolve from ENV first, then cfg_helper ----------
def _resolve_channel_id(c, key: str) -> int:
# 1) ENV always wins
env_key = f"SHAI_{key.upper()}"
raw = os.getenv(env_key, "").strip().strip('"').strip("'")
if raw.isdigit():
return int(raw)
# 2) Try cfg_helper (if it happens to know)
try:
v = int(c.int(key, 0))
if v:
return v
except Exception:
pass
# 3) Last resort: legacy bot.config shapes
try:
# bot.config like dict
v = int(getattr(c, "get", lambda *_: 0)(key, 0))
if v:
return v
except Exception:
pass
return 0
async def _guild_selfcheck(g: discord.Guild, c):
problems = []
def _need_channel(id_key, *perms):
cid = _resolve_channel_id(c, id_key)
if not cid:
problems.append(f"Missing config key: {id_key}")
return
ch = g.get_channel(cid)
if not ch:
problems.append(f"Channel not found: {id_key}={cid}")
return
me = g.me
p = ch.permissions_for(me)
for perm in perms:
if not getattr(p, perm, False):
problems.append(f"Missing permission on #{ch.name}: {perm}")
_need_channel('mod_channel_id', 'read_messages', 'send_messages', 'add_reactions', 'read_message_history')
_need_channel('modlog_channel_id', 'read_messages', 'send_messages')
_need_channel('pirates_list_channel_id', 'read_messages', 'send_messages')
if problems:
print(f"[SelfCheck:{g.name}]")
for p in problems:
print(" -", p)
# ---------- events ----------
@bot.event
async def on_ready():
import asyncio # safe inside function; ensures availability
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
print("[Intents] members:", bot.intents.members,
"/ message_content:", bot.intents.message_content,
"/ voice_states:", bot.intents.voice_states)
env_cfg = cfg_helper(bot)
# Per-guild permission sanity checks (env-aware)
try:
await asyncio.gather(*[_guild_selfcheck(g, env_cfg) for g in bot.guilds])
except Exception as e:
print("[SelfCheck] failed:", repr(e))
# Slash command sync (env-aware dev guild) + always print what exists after sync
try:
dev_gid = env_cfg.get("dev_guild_id")
if dev_gid:
guild = bot.get_guild(int(dev_gid))
if guild:
synced = await bot.tree.sync(guild=guild)
print(f"[Slash] Synced {len(synced)} commands to {guild.name} ({guild.id})")
else:
synced = await bot.tree.sync()
print(f"[Slash] Synced {len(synced)} commands globally (dev_guild_id not in cache)")
else:
synced = await bot.tree.sync()
print(f"[Slash] Synced {len(synced)} commands globally")
# --- List what actually exists after sync ---
def _fmt_cmds(cmds):
try:
names = [f"/{c.name}" for c in cmds]
return ", ".join(names) if names else "(none)"
except Exception:
return "(unreadable)"
# Global commands
try:
global_cmds = await bot.tree.fetch_commands()
print(f"[Slash] Global commands ({len(global_cmds)}): {_fmt_cmds(global_cmds)}")
except Exception as e:
print("[Slash] Failed to fetch global commands:", repr(e))
# Target guilds to inspect: dev + home (if provided)
target_gids = set()
if dev_gid:
try:
target_gids.add(int(dev_gid))
except Exception:
pass
home_gid = env_cfg.get("home_guild_id")
if home_gid:
try:
target_gids.add(int(home_gid))
except Exception:
pass
for gid in sorted(target_gids):
g = bot.get_guild(gid)
if not g:
print(f"[Slash] Guild {gid}: not in cache; cannot fetch commands.")
continue
try:
g_cmds = await bot.tree.fetch_commands(guild=g)
print(f"[Slash] {g.name} ({g.id}) guild commands ({len(g_cmds)}): {_fmt_cmds(g_cmds)}")
except Exception as e:
print(f"[Slash] Failed to fetch commands for guild {gid}:", repr(e))
# --- end list ---
except Exception as e:
print("[Slash] Sync failed:", repr(e))
# Post boot status message
try:
await post_boot_notice(bot)
except Exception as e:
print("[BootNotice] failed:", repr(e))
# ---------- Auto-discover extensions ----------
modules_path = pathlib.Path(__file__).parent / "modules"
extensions = []
for folder in modules_path.iterdir():
if not folder.is_dir():
continue
# skip non-cog helpers under modules/common
if folder.name == "common":
continue
for file in folder.glob("*.py"):
if file.name == "__init__.py":
continue
extensions.append(f"modules.{folder.name}.{file.stem}")
def _install_signal_handlers(loop, bot_obj):
def _graceful(*_):
loop.create_task(bot_obj.close())
for s in (signal.SIGTERM, signal.SIGINT):
try:
loop.add_signal_handler(s, _graceful)
except NotImplementedError:
pass # Windows
async def main():
print(f"[STARTUP] ShaiWatcher booting v{VERSION}")
async with bot:
for ext in extensions:
try:
await bot.load_extension(ext)
print(f"[Modules] Loaded: {ext}")
except Exception as e:
print(f"[Modules] Failed to load {ext}:", repr(e))
loop = asyncio.get_running_loop()
_install_signal_handlers(loop, bot)
await bot.start(TOKEN)
if __name__ == "__main__":
asyncio.run(main())