175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
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.a3"
|
||
|
||
# ---------- 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):
|
||
d = cfg['DEFAULT']
|
||
for k, v in os.environ.items():
|
||
if not k.startswith('SHAI_'):
|
||
continue
|
||
key = k[5:].lower()
|
||
if key == 'data':
|
||
key = 'data_file'
|
||
if v is None:
|
||
continue
|
||
vv = str(v).strip().strip('"').strip("'")
|
||
if key == 'data_file' and not vv:
|
||
continue
|
||
d[key] = vv
|
||
if not d.get('data_file', '').strip():
|
||
d['data_file'] = '/data/data.json'
|
||
|
||
print("[Config] SHAI_CONFIG:", os.getenv('SHAI_CONFIG', '(unset)'))
|
||
print("[Config] DEFAULT keys now:", list(config['DEFAULT'].keys()))
|
||
|
||
# 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 ----------
|
||
|
||
ddefault = config['DEFAULT']
|
||
data_file = (ddefault.get('data_file', '') or '').strip() or '/data/data.json'
|
||
if not TOKEN:
|
||
print("[Config] WARNING: DISCORD_TOKEN not set (env). Bot will fail to log in.")
|
||
print(f"[Config] Using data_file: {data_file}")
|
||
|
||
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())
|