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" — "
try:
+ msg = line if not desc else f"{line}\n_{desc}_"
await ch.send(msg, allowed_mentions=discord.AllowedMentions.none())
except Exception:
pass