diff --git a/bot.py b/bot.py index 80bf758..a03ad7f 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) -VERSION = "0.3.9.4.a1" +VERSION = "0.3.9.4.a2" # ---------- Env loading ---------- diff --git a/modules/common/boot_notice.py b/modules/common/boot_notice.py index 5635f6e..a45e8b6 100644 --- a/modules/common/boot_notice.py +++ b/modules/common/boot_notice.py @@ -1,12 +1,37 @@ # modules/common/boot_notice.py import os +import re +import html import discord import aiohttp import xml.etree.ElementTree as ET from modules.common.settings import cfg -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.""" +# ---------------- RSS helpers ---------------- + +def _strip_html_keep_text(s: str) -> str: + """Remove HTML tags, unescape entities, collapse excessive blank lines.""" + if not s: + return "" + # Replace
and

with newlines before stripping tags + s = re.sub(r'(?i)<\s*br\s*/?\s*>', '\n', s) + s = re.sub(r'(?i)', '\n', s) + s = re.sub(r'(?i)<\s*p\s*>', '', s) + # Strip all remaining tags + s = re.sub(r'<[^>]+>', '', s) + # Unescape HTML entities + s = html.unescape(s) + # Trim trailing spaces on each line + s = '\n'.join(line.rstrip() for line in s.splitlines()) + # Collapse 3+ blank lines to max 2 + s = re.sub(r'\n{3,}', '\n\n', s).strip() + return s + +async def _fetch_latest_commit_from_rss(rss_url: str): + """ + Return (subject:str|None, body:str|None) from the newest item in a Gitea/Git RSS feed. + Best-effort. We avoid posting links/usernames and keep content human-friendly. + """ try: timeout = aiohttp.ClientTimeout(total=8) async with aiohttp.ClientSession(timeout=timeout) as sess: @@ -18,41 +43,112 @@ async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | Non 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 + # Gitea typically puts commit message (possibly HTML-wrapped) in + desc_raw = (item.findtext('description') or '').strip() + body = _strip_html_keep_text(desc_raw) + + # Some feeds stuff noise/usernames into title; keep it short & human: + # If title contains " pushed " etc., try to fall back to first line of body. + if title and re.search(r'\b(pushed|commit|committed)\b', title, re.I): + # If body has a first line that looks like a summary, use it. + first_line = body.splitlines()[0].strip() if body else "" + if first_line: + title = first_line + + # Clean again in case any entities lingered in title + title = _strip_html_keep_text(title) + + # If title empty but body present, use the first non-empty line of body as title. + if not title and body: + for line in body.splitlines(): + if line.strip(): + title = line.strip() + break + + return (title or None), (body or None) except Exception: return None, None +# ---------------- Status helpers ---------------- + +def _build_status_line(status: str, old_v: str, new_v: str, desc: str) -> str | None: + """ + Return a short human-readable boot status line, or None if nothing to post. + Known statuses: + - fetched_new -> updated & booted + - cached_no_update -> booted cached, no update + - cache_only_error -> booted cached, repo unavailable + - scheduled_restart -> a scheduled restart was initiated + """ + status = (status or "").strip() + old_v = (old_v or "").strip() + new_v = (new_v or "").strip() + + if status == "fetched_new": + line = f"βœ… Booted new version: v{old_v or '0.0.0.0'} β†’ **v{new_v}**" + elif status == "cached_no_update": + line = f"🟒 Booted cached version: **v{new_v}** β€” no new update found" + elif status == "cache_only_error": + line = f"🟑 Booted cached version: **v{new_v}** β€” repository not accessible" + elif status == "scheduled_restart": + line = "πŸ•’ Scheduled restart executed" + else: + return None + + return f"{line}\n_{desc.strip()}_" if desc else line + +def _only_version_and_details(subject: str | None, body: str | None) -> str | None: + """ + Format to 'Version number' (bold) + 'Version details' (md). + We try to extract a version-like token from subject; otherwise we use subject as-is. + """ + if not subject and not body: + return None + + version = None + if subject: + # Try to find a version-like token (v1.2.3 or 1.2.3.4 etc.) + m = re.search(r'\bv?(\d+\.\d+(?:\.\d+){0,2})\b', subject) + if m: + version = m.group(0) + else: + # Fall back to the subject line itself as "version-ish" title + version = subject.strip() + + if version and body: + return f"**{version}**\n{body.strip()}" + if version: + return f"**{version}**" + # No subject/version, only body + return body.strip() if body else None + +# ---------------- Main entry ---------------- + 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. + Posts concise boot status to the modlog channel. + - Always: one status line (if SHAI_BOOT_STATUS is set to a known value). + - If SHAI_BOOT_STATUS == 'fetched_new': fetch latest commit from SHAI_REPO_RSS + and post ONLY the commit message (version number + markdown details). """ - status = os.getenv("SHAI_BOOT_STATUS", "").strip() # 'fetched_new' | 'cached_no_update' | 'cache_only_error' | '' + status = os.getenv("SHAI_BOOT_STATUS", "").strip() # fetched_new | cached_no_update | cache_only_error | scheduled_restart | '' desc = os.getenv("SHAI_BOOT_DESC", "").strip() old_v = os.getenv("SHAI_BOOT_OLD", "").strip() new_v = os.getenv("SHAI_BOOT_NEW", "").strip() + rss = os.getenv("SHAI_REPO_RSS", "").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 not line: + # Build status line + status_line = _build_status_line(status, old_v, new_v, desc) + if not status_line: return # nothing to say - # Read modlog channel from ENV/INI via helper + # Resolve modlog channel modlog_channel_id = cfg(bot).int('modlog_channel_id', 0) if not modlog_channel_id: return - # Find channel across guilds ch = None for g in bot.guilds: ch = g.get_channel(modlog_channel_id) @@ -61,8 +157,18 @@ async def post_boot_notice(bot): if not ch: return + # Post the status try: - msg = line if not desc else f"{line}\n_{desc}_" - await ch.send(msg, allowed_mentions=discord.AllowedMentions.none()) + await ch.send(status_line, allowed_mentions=discord.AllowedMentions.none()) except Exception: - pass + return + + # If we fetched & booted a new version, follow up with the commit message from RSS (no env details assumed). + if status == "fetched_new" and rss: + subj, body = await _fetch_latest_commit_from_rss(rss) + commit_msg = _only_version_and_details(subj, body) + if commit_msg: + try: + await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none()) + except Exception: + pass