shaiwatcher/bot.py
Franz Rolfsvaag 4e77cddc92 0.3.9.2.a10
Restructured runtime environment variables passing to cogs
2025-08-10 21:46:15 +02:00

193 lines
5.9 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.2.a10"
# ---------- 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():
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)
# DEBUG: show what cfg resolves
for k in ("mod_channel_id", "modlog_channel_id", "pirates_list_channel_id"):
try:
print(f"[DEBUG cfg] {k}: str={env_cfg.get(k)} int={env_cfg.int(k, 0)}")
except Exception as e:
print(f"[DEBUG cfg] {k}: error={e!r}")
# Per-guild permission sanity checks (env-aware)
await asyncio.gather(*[_guild_selfcheck(g, env_cfg) for g in bot.guilds])
# Slash command sync (env-aware dev guild)
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}")
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")
except Exception as e:
print("[Slash] Sync failed:", repr(e))
# Post boot status message (wrapper/env-driven or RSS fallback)
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())