shaiwatcher/bot.py
Franz Rolfsvaag e6ccc86629 0.3.9.2.a2
Restart-related patch
2025-08-10 18:06:09 +02:00

169 lines
5.4 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.a2"
# ---------- Env & config loading ----------
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN', '').strip()
CONFIG_PATH = os.getenv('SHAI_CONFIG', '/config/settings.conf')
config = ConfigParser()
read_files = config.read(CONFIG_PATH)
if not read_files:
print(f"[Config] INFO: no config at {CONFIG_PATH} (or unreadable). Will rely on env + defaults.")
if 'DEFAULT' not in config:
config['DEFAULT'] = {}
def _overlay_env_into_config(cfg: ConfigParser):
"""
Overlay all SHAI_* environment variables into cfg['DEFAULT'] so env wins.
Also accept SHAI_DATA_FILE or SHAI_DATA for data_file.
"""
d = cfg['DEFAULT']
for k, v in os.environ.items():
if not k.startswith('SHAI_'):
continue
key = k[5:].lower() # drop 'SHAI_'
if key == 'data':
key = 'data_file'
d[key] = str(v)
if not d.get('data_file', '').strip():
d['data_file'] = '/data/data.json'
# IMPORTANT: apply env overlay BEFORE we read values from config
_overlay_env_into_config(config)
# ---------- 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 ----------
data_file = config['DEFAULT']['data_file'] # guaranteed present by overlay
if not TOKEN:
print("[Config] WARNING: DISCORD_TOKEN not set (env). Bot will fail to log in.")
bot = commands.Bot(command_prefix='!', intents=intents)
bot.config = config
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)
# Per-guild permission sanity checks (console log)
await asyncio.gather(*[_guild_selfcheck(g, bot.config['DEFAULT']) for g in bot.guilds])
# Slash command sync
try:
dev_gid = bot.config['DEFAULT'].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 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 # Windows
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())