import os, signal, asyncio, pathlib import discord from discord.ext import commands from dotenv import load_dotenv from configparser import ConfigParser from data_manager import DataManager from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) VERSION = "0.3.9.2.a5" # ---------- Env & config loading ---------- load_dotenv() def _get_env(name: str, default: str = "") -> str: v = os.getenv(name, "") # normalize stray quotes/whitespace Portainer sometimes injects v = (v or "").strip().strip('"').strip("'") return v if v else default TOKEN = _get_env("DISCORD_TOKEN") DATA_FILE = _get_env("SHAI_DATA") or _get_env("SHAI_DATA_FILE") or "/data/data.json" CHECK_TIME_UTC = _get_env("CHECK_TIME_UTC", "03:00") IGNORE_TEST_LEVEL = int(_get_env("IGNORE_TEST_LEVEL", "1")) SHAI_HOME_GUILD_ID = _get_env("SHAI_HOME_GUILD_ID") print("[Config] DISCORD_TOKEN set:", bool(TOKEN)) print("[Config] DATA_FILE:", DATA_FILE) print("[Config] CHECK_TIME_UTC:", CHECK_TIME_UTC) print("[Config] IGNORE_TEST_LEVEL:", IGNORE_TEST_LEVEL) print("[Config] SHAI_HOME_GUILD_ID:", SHAI_HOME_GUILD_ID or "(unset)") # ---------- 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) # If you still want to pass “config” around, put a tiny dict on bot: bot.config = { "check_time_utc": CHECK_TIME_UTC, "ignore_test_level": IGNORE_TEST_LEVEL, "home_guild_id": SHAI_HOME_GUILD_ID, } # Ensure the data path exists and file is seeded if missing os.makedirs(os.path.dirname(DATA_FILE), 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 helpers ---------- async def _guild_selfcheck(g: discord.Guild, cfg): problems = [] def _need_channel(id_key, *perms): raw = cfg.get(id_key) if not raw: problems.append(f"Missing config key: {id_key}") return try: cid = int(raw) except Exception: problems.append(f"Bad channel id for {id_key}: {raw}") 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) await asyncio.gather(*[_guild_selfcheck(g, bot.config) for g in bot.guilds]) # Slash sync — now reading from dict try: dev_gid = bot.config.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)) 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 folder.is_dir(): 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): def _graceful(*_): loop.create_task(bot.close()) for s in (signal.SIGTERM, signal.SIGINT): try: loop.add_signal_handler(s, _graceful) except NotImplementedError: pass async def main(): 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())