diff --git a/bot.py b/bot.py index 5ca6805..0fe2a64 100644 --- a/bot.py +++ b/bot.py @@ -1,20 +1,14 @@ +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 -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" +# Version consists of: +# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) +VERSION = "0.3.9.2.a2" # ---------- Env & config loading ---------- @@ -27,8 +21,6 @@ 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'] = {} @@ -38,20 +30,17 @@ def _overlay_env_into_config(cfg: ConfigParser): 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 + 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' -# Apply overlay so env takes precedence everywhere +# IMPORTANT: apply env overlay BEFORE we read values from config _overlay_env_into_config(config) # ---------- Discord intents ---------- @@ -108,87 +97,16 @@ async def _guild_selfcheck(g: discord.Guild, cfg): 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: - root = ET.fromstring(text) - item = root.find('./channel/item') - if item is None: - return None, None - title = (item.findtext('title') or '').strip() - link = (item.findtext('link') or '').strip() - # Try to extract short sha from link tail if it's a commit URL - sha = None - if '/commit/' in link: - sha = link.rsplit('/commit/', 1)[-1][:7] - # Many Gitea feeds put the commit subject in - subject = title if title else None - return subject, sha - except Exception: - return None, None - -# ---------- boot notice ---------- - -async def _maybe_post_boot_notice(bot): - status = os.getenv("SHAI_BOOT_STATUS", "") - if not status: - return - desc = os.getenv("SHAI_BOOT_DESC", "") - old_v = os.getenv("SHAI_BOOT_OLD", "") - new_v = os.getenv("SHAI_BOOT_NEW", "") - - if status == "fetched_new": - line = f"Successfully fetched, cached, and booted new version: v{old_v or '0.0.0.0'} -> v{new_v}" - elif status == "cached_no_update": - line = f"Successfully booted from cached version: v{new_v}. No new update found" - else: - line = f"Successfully booted from cached version: v{new_v}. Program repository not accessible!" - - ch_id = int(bot.config['DEFAULT'].get('modlog_channel_id', "0") or 0) - ch = None - for g in bot.guilds: - ch = g.get_channel(ch_id) - if ch: break - if ch: - try: - msg = line if not desc else f"{line}\n_{desc}_" - await ch.send(msg, allowed_mentions=discord.AllowedMentions.none()) - except Exception: - pass - -async def _post_boot_notice(): - - msg = f"Self-update and reboot successful! (v.{VERSION})" - - ch_id_raw = bot.config['DEFAULT'].get('modlog_channel_id', '') - try: - ch_id = int(ch_id_raw) if ch_id_raw else 0 - except Exception: - ch_id = 0 - if not ch_id: - return - for g in bot.guilds: - ch = g.get_channel(ch_id) - if ch: - try: - await ch.send(msg) - except Exception: - pass - break - # ---------- 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) + 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 @@ -207,8 +125,8 @@ async def on_ready(): print(f"[Slash] Synced {len(synced)} commands globally") except Exception as e: print("[Slash] Sync failed:", repr(e)) - - # Post BSM if present + + # Post boot status message (wrapper/env-driven or RSS fallback) try: await post_boot_notice(bot) except Exception as e: @@ -227,7 +145,6 @@ for folder in modules_path.iterdir(): def _install_signal_handlers(loop, bot): def _graceful(*_): - # ask discord.py to close cleanly loop.create_task(bot.close()) for s in (signal.SIGTERM, signal.SIGINT): try: diff --git a/modules/common/boot_notice.py b/modules/common/boot_notice.py index f3830dd..2dd27bb 100644 --- a/modules/common/boot_notice.py +++ b/modules/common/boot_notice.py @@ -1,18 +1,65 @@ -# modules/common/boot_notice.py import os -import time import discord +import aiohttp +import xml.etree.ElementTree as ET -async def post_boot_notice(bot: discord.Client): - # Needs modlog_channel_id in config/env - cfg = bot.config['DEFAULT'] - ch_raw = cfg.get('modlog_channel_id') or os.getenv('SHAI_MODLOG_CHANNEL_ID') - if not ch_raw: - return +async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | None]: + """Best-effort: read latest commit subject + short sha from a Gitea RSS feed.""" try: - ch_id = int(ch_raw) + timeout = aiohttp.ClientTimeout(total=8) + async with aiohttp.ClientSession(timeout=timeout) as sess: + async with sess.get(rss_url) as resp: + if resp.status != 200: + return None, None + text = await resp.text() + root = ET.fromstring(text) + item = root.find('./channel/item') + if item is None: + return None, None + title = (item.findtext('title') or '').strip() + link = (item.findtext('link') or '').strip() + sha = link.rsplit('/commit/', 1)[-1][:7] if '/commit/' in link else None + return (title or None), sha except Exception: + return None, None + +async def post_boot_notice(bot): + """ + Posts a boot status message to the configured modlog channel. + Primary source: SHAI_BOOT_* env vars set by the wrapper. + Fallback: if absent, and SHAI_REPO_RSS is set, show the latest commit subject. + """ + status = os.getenv("SHAI_BOOT_STATUS", "").strip() # 'fetched_new' | 'cached_no_update' | 'cache_only_error' | '' + desc = os.getenv("SHAI_BOOT_DESC", "").strip() + old_v = os.getenv("SHAI_BOOT_OLD", "").strip() + new_v = os.getenv("SHAI_BOOT_NEW", "").strip() + + line = None + if status == "fetched_new": + line = f"Successfully fetched, cached, and booted new version: v{old_v or '0.0.0.0'} → v{new_v}" + elif status == "cached_no_update": + line = f"Successfully booted from cached version: v{new_v}. No new update found" + elif status == "cache_only_error": + line = f"Successfully booted from cached version: v{new_v}. Program repository not accessible!" + + # If wrapper didn’t set a status, optionally show latest commit subject from RSS (if provided) + if not line: + rss = os.getenv("SHAI_REPO_RSS", "").strip() + if rss: + subject, sha = await _fetch_latest_subject_sha(rss) + if subject and len(subject) > 5: + line = f"Booted (no BSM env). Latest commit: {subject}" + (f" ({sha})" if sha else "") + + if not line: + return # nothing to say + + try: + ch_id = int(bot.config['DEFAULT'].get('modlog_channel_id', "0") or 0) + except Exception: + ch_id = 0 + if not ch_id: return + ch = None for g in bot.guilds: ch = g.get_channel(ch_id) @@ -21,25 +68,8 @@ async def post_boot_notice(bot: discord.Client): if not ch: return - status = os.getenv("SHAI_BOOT_STATUS", "").strip() - oldver = os.getenv("SHAI_BOOT_OLDVER", "").strip() - newver = os.getenv("SHAI_BOOT_NEWVER", "").strip() - commit = os.getenv("SHAI_BUILD_COMMIT", "").strip() - subject = os.getenv("SHAI_BUILD_SUBJECT", "").strip() - - if not status: - return - - parts = [f"**Boot**: {status}"] - if oldver or newver: - parts.append(f"**Version**: {oldver or '?'} → {newver or '?'}") - if commit: - parts.append(f"**Commit**: `{commit}`") - if subject and len(subject) > 5: - parts.append(f"**Note**: {subject}") - - msg = " | ".join(parts) + f" — <t:{int(time.time())}:R>" try: + msg = line if not desc else f"{line}\n_{desc}_" await ch.send(msg, allowed_mentions=discord.AllowedMentions.none()) except Exception: pass