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
import pathlib
import os, signal, asyncio, xml.etree.ElementTree as ET
import aiohttp
# Version consists of the following:
# Major version
# Enhancement version
# Minor version
# Patch version
# Test/Dev version -> Does not trigger automatic update
VERSION="0.3.9.1.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.")
# Ensure DEFAULT section exists
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']
# Map SHAI_* -> lower-case keys (e.g. SHAI_MOD_CHANNEL_ID -> 'mod_channel_id')
for k, v in os.environ.items():
if not k.startswith('SHAI_'):
continue
key = k[5:].lower() # drop 'SHAI_' prefix
if key == 'data':
key = 'data_file'
d[key] = str(v)
if not d.get('data_file', '').strip():
d['data_file'] = '/data/data.json'
# Apply overlay so env takes precedence everywhere
_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)
async def _fetch_latest_from_rss(url: str):
try:
timeout = aiohttp.ClientTimeout(total=8)
async with aiohttp.ClientSession(timeout=timeout) as sess:
async with sess.get(url) as resp:
if resp.status != 200:
return None, None
text = await resp.text()
# Gitea RSS structure: