0.3.9.2.a2
Restart-related patch
This commit is contained in:
		
							parent
							
								
									36939efac3
								
							
						
					
					
						commit
						e6ccc86629
					
				
							
								
								
									
										105
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								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: <rss><channel><item>…</item></channel></rss>
 | 
			
		||||
        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 <title>
 | 
			
		||||
        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
 | 
			
		||||
@ -208,7 +126,7 @@ async def on_ready():
 | 
			
		||||
    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:
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user