shaiwatcher/bot.py
Franz Rolfsvaag 9b94280e8b 0.5.1.2.a3
- Small patch to repair orphaned fedaykin requests
2025-08-25 23:57:39 +02:00

220 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.5.1.2.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("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())