shaiwatcher/bot.py
Franz Rolfsvaag 7222239774 .
2025-08-10 20:36:13 +02:00

169 lines
5.5 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 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; doesnt 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():
# 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):
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())