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; doesn’t trigger auto update) VERSION = "0.5.1.2.a2" # ---------- 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("DATA_FILE") or "./data/data.json" print("[Config] DISCORD_TOKEN set:", bool(TOKEN)) print("[Config] DATA_FILE:", DATA_FILE) # ---------- Ensure data path exists (fallback if not writable) ---------- data_dir = os.path.dirname(DATA_FILE) or "." try: os.makedirs(data_dir, exist_ok=True) except PermissionError: fallback = "./data/data.json" print(f"[Config] No permission to create '{data_dir}'. Falling back to {fallback}") DATA_FILE = fallback data_dir = os.path.dirname(DATA_FILE) os.makedirs(data_dir, exist_ok=True) if not os.path.exists(DATA_FILE): with open(DATA_FILE, "w", encoding="utf-8") as f: f.write("{}") # ---------- 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: """ Resolve channel IDs from the runtime settings store (cfg), with a final fallback to legacy bot.config['DEFAULT'] if present. No SHAI_* env usage. """ try: v = int(c.int(key, 0)) if v: return v except Exception: pass try: # legacy DEFAULT mapping (ConfigParser-like or our shim) 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') _need_channel('dd_channel_id', 'read_messages', 'send_messages', 'read_message_history') if problems: print(f"[SelfCheck:{g.name}]") for p in problems: print(" -", p) # ---------- events ---------- @bot.event async def on_ready(): import asyncio 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 scope & sync ---------- try: # env_cfg already exists above in on_ready() gid = env_cfg.int("home_guild_id", 0) if gid > 0: print(f"[Slash] Mode: GUILD-ONLY → {gid}") guild_obj = discord.Object(id=gid) # Copy all currently-loaded global commands to HOME guild bot.tree.copy_global_to(guild=guild_obj) g_cmds = await bot.tree.sync(guild=guild_obj) g_names = ", ".join(f"/{c.name}" for c in g_cmds) if g_cmds else "(none)" print(f"[Slash] Synced {len(g_cmds)} commands to guild {gid}: {g_names}") # Clear global so only guild-scoped remain bot.tree.clear_commands(guild=None) cleared = await bot.tree.sync() # push empty global set print(f"[Slash] Cleared global commands (now {len(cleared)}).") # Debug: list actual state after sync try: global_cmds = await bot.tree.fetch_commands() print(f"[Slash] Global commands ({len(global_cmds)}): {', '.join(f'/{c.name}' for c in global_cmds) or '(none)'}") except Exception as e: print("[Slash] Failed to fetch global commands:", repr(e)) try: g_cmds = await bot.tree.fetch_commands(guild=guild_obj) print(f"[Slash] Guild {gid} commands ({len(g_cmds)}): {', '.join(f'/{c.name}' for c in g_cmds) or '(none)'}") except Exception as e: print(f"[Slash] Failed to fetch commands for guild {gid}:", repr(e)) else: print("[Slash] Mode: GLOBAL (HOME_GUILD_ID not set)") global_cmds = await bot.tree.sync() names = ", ".join(f"/{c.name}" for c in global_cmds) if global_cmds else "(none)" print(f"[Slash] Synced {len(global_cmds)} commands globally: {names}") except Exception as e: print("[Slash] Sync 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())