0.3.9.2.a2
Restart-related patch
This commit is contained in:
		
							parent
							
								
									36939efac3
								
							
						
					
					
						commit
						e6ccc86629
					
				
							
								
								
									
										107
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								bot.py
									
									
									
									
									
								
							@ -1,20 +1,14 @@
 | 
				
			|||||||
 | 
					import os, signal, asyncio, pathlib
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord.ext import commands
 | 
					from discord.ext import commands
 | 
				
			||||||
from dotenv import load_dotenv
 | 
					from dotenv import load_dotenv
 | 
				
			||||||
from configparser import ConfigParser
 | 
					from configparser import ConfigParser
 | 
				
			||||||
from data_manager import DataManager
 | 
					from data_manager import DataManager
 | 
				
			||||||
from modules.common.boot_notice import post_boot_notice
 | 
					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:
 | 
					# Version consists of:
 | 
				
			||||||
# Major version
 | 
					# Major.Enhancement.Minor.Patch.Test  (Test is alphanumeric; doesn’t trigger auto update)
 | 
				
			||||||
# Enhancement version
 | 
					VERSION = "0.3.9.2.a2"
 | 
				
			||||||
# Minor version
 | 
					 | 
				
			||||||
# Patch version
 | 
					 | 
				
			||||||
# Test/Dev version -> Does not trigger automatic update
 | 
					 | 
				
			||||||
VERSION="0.3.9.1.a2"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------- Env & config loading ----------
 | 
					# ---------- Env & config loading ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,8 +21,6 @@ config = ConfigParser()
 | 
				
			|||||||
read_files = config.read(CONFIG_PATH)
 | 
					read_files = config.read(CONFIG_PATH)
 | 
				
			||||||
if not read_files:
 | 
					if not read_files:
 | 
				
			||||||
    print(f"[Config] INFO: no config at {CONFIG_PATH} (or unreadable). Will rely on env + defaults.")
 | 
					    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:
 | 
					if 'DEFAULT' not in config:
 | 
				
			||||||
    config['DEFAULT'] = {}
 | 
					    config['DEFAULT'] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,20 +30,17 @@ def _overlay_env_into_config(cfg: ConfigParser):
 | 
				
			|||||||
    Also accept SHAI_DATA_FILE or SHAI_DATA for data_file.
 | 
					    Also accept SHAI_DATA_FILE or SHAI_DATA for data_file.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    d = cfg['DEFAULT']
 | 
					    d = cfg['DEFAULT']
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Map SHAI_* -> lower-case keys (e.g. SHAI_MOD_CHANNEL_ID -> 'mod_channel_id')
 | 
					 | 
				
			||||||
    for k, v in os.environ.items():
 | 
					    for k, v in os.environ.items():
 | 
				
			||||||
        if not k.startswith('SHAI_'):
 | 
					        if not k.startswith('SHAI_'):
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
        key = k[5:].lower()  # drop 'SHAI_' prefix
 | 
					        key = k[5:].lower()  # drop 'SHAI_'
 | 
				
			||||||
        if key == 'data':
 | 
					        if key == 'data':
 | 
				
			||||||
            key = 'data_file'
 | 
					            key = 'data_file'
 | 
				
			||||||
        d[key] = str(v)
 | 
					        d[key] = str(v)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not d.get('data_file', '').strip():
 | 
					    if not d.get('data_file', '').strip():
 | 
				
			||||||
        d['data_file'] = '/data/data.json'
 | 
					        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)
 | 
					_overlay_env_into_config(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------- Discord intents ----------
 | 
					# ---------- Discord intents ----------
 | 
				
			||||||
@ -108,87 +97,16 @@ async def _guild_selfcheck(g: discord.Guild, cfg):
 | 
				
			|||||||
        for p in problems:
 | 
					        for p in problems:
 | 
				
			||||||
            print(" -", p)
 | 
					            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 ----------
 | 
					# ---------- events ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bot.event
 | 
					@bot.event
 | 
				
			||||||
async def on_ready():
 | 
					async def on_ready():
 | 
				
			||||||
    print(f"Logged in as {bot.user} (ID: {bot.user.id})")
 | 
					    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])
 | 
					    await asyncio.gather(*[_guild_selfcheck(g, bot.config['DEFAULT']) for g in bot.guilds])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Slash command sync
 | 
					    # Slash command sync
 | 
				
			||||||
@ -207,8 +125,8 @@ async def on_ready():
 | 
				
			|||||||
            print(f"[Slash] Synced {len(synced)} commands globally")
 | 
					            print(f"[Slash] Synced {len(synced)} commands globally")
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print("[Slash] Sync failed:", repr(e))
 | 
					        print("[Slash] Sync failed:", repr(e))
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
    # Post BSM if present
 | 
					    # Post boot status message (wrapper/env-driven or RSS fallback)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        await post_boot_notice(bot)
 | 
					        await post_boot_notice(bot)
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
@ -227,7 +145,6 @@ for folder in modules_path.iterdir():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def _install_signal_handlers(loop, bot):
 | 
					def _install_signal_handlers(loop, bot):
 | 
				
			||||||
    def _graceful(*_):
 | 
					    def _graceful(*_):
 | 
				
			||||||
        # ask discord.py to close cleanly
 | 
					 | 
				
			||||||
        loop.create_task(bot.close())
 | 
					        loop.create_task(bot.close())
 | 
				
			||||||
    for s in (signal.SIGTERM, signal.SIGINT):
 | 
					    for s in (signal.SIGTERM, signal.SIGINT):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,65 @@
 | 
				
			|||||||
# modules/common/boot_notice.py
 | 
					 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
 | 
					import aiohttp
 | 
				
			||||||
 | 
					import xml.etree.ElementTree as ET
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def post_boot_notice(bot: discord.Client):
 | 
					async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | None]:
 | 
				
			||||||
    # Needs modlog_channel_id in config/env
 | 
					    """Best-effort: read latest commit subject + short sha from a Gitea RSS feed."""
 | 
				
			||||||
    cfg = bot.config['DEFAULT']
 | 
					 | 
				
			||||||
    ch_raw = cfg.get('modlog_channel_id') or os.getenv('SHAI_MODLOG_CHANNEL_ID')
 | 
					 | 
				
			||||||
    if not ch_raw:
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    try:
 | 
					    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:
 | 
					    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
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ch = None
 | 
					    ch = None
 | 
				
			||||||
    for g in bot.guilds:
 | 
					    for g in bot.guilds:
 | 
				
			||||||
        ch = g.get_channel(ch_id)
 | 
					        ch = g.get_channel(ch_id)
 | 
				
			||||||
@ -21,25 +68,8 @@ async def post_boot_notice(bot: discord.Client):
 | 
				
			|||||||
    if not ch:
 | 
					    if not ch:
 | 
				
			||||||
        return
 | 
					        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:
 | 
					    try:
 | 
				
			||||||
 | 
					        msg = line if not desc else f"{line}\n_{desc}_"
 | 
				
			||||||
        await ch.send(msg, allowed_mentions=discord.AllowedMentions.none())
 | 
					        await ch.send(msg, allowed_mentions=discord.AllowedMentions.none())
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user