0.3.9.4.a2

Added better startup messages being sent to the Discord modlog channel
This commit is contained in:
Franz Rolfsvaag 2025-08-11 00:39:18 +02:00
parent b74002e69f
commit 6e85897ca8
2 changed files with 130 additions and 24 deletions

2
bot.py
View File

@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
# Version consists of: # Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update) # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.4.a1" VERSION = "0.3.9.4.a2"
# ---------- Env loading ---------- # ---------- Env loading ----------

View File

@ -1,12 +1,37 @@
# modules/common/boot_notice.py # modules/common/boot_notice.py
import os import os
import re
import html
import discord import discord
import aiohttp import aiohttp
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from modules.common.settings import cfg from modules.common.settings import cfg
async def _fetch_latest_subject_sha(rss_url: str) -> tuple[str | None, str | None]: # ---------------- RSS helpers ----------------
"""Best-effort: read latest commit subject + short sha from a Gitea RSS feed."""
def _strip_html_keep_text(s: str) -> str:
"""Remove HTML tags, unescape entities, collapse excessive blank lines."""
if not s:
return ""
# Replace <br> and <p> with newlines before stripping tags
s = re.sub(r'(?i)<\s*br\s*/?\s*>', '\n', s)
s = re.sub(r'(?i)</\s*p\s*>', '\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: try:
timeout = aiohttp.ClientTimeout(total=8) timeout = aiohttp.ClientTimeout(total=8)
async with aiohttp.ClientSession(timeout=timeout) as sess: 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') item = root.find('./channel/item')
if item is None: if item is None:
return None, None return None, None
title = (item.findtext('title') or '').strip() title = (item.findtext('title') or '').strip()
link = (item.findtext('link') or '').strip() # Gitea typically puts commit message (possibly HTML-wrapped) in <description>
sha = link.rsplit('/commit/', 1)[-1][:7] if '/commit/' in link else None desc_raw = (item.findtext('description') or '').strip()
return (title or None), sha 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: except Exception:
return None, None 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): async def post_boot_notice(bot):
""" """
Posts a boot status message to the configured modlog channel. Posts concise boot status to the modlog channel.
Primary source: SHAI_BOOT_* env vars set by the wrapper. - Always: one status line (if SHAI_BOOT_STATUS is set to a known value).
Fallback: if absent, and SHAI_REPO_RSS is set, show the latest commit subject. - 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() desc = os.getenv("SHAI_BOOT_DESC", "").strip()
old_v = os.getenv("SHAI_BOOT_OLD", "").strip() old_v = os.getenv("SHAI_BOOT_OLD", "").strip()
new_v = os.getenv("SHAI_BOOT_NEW", "").strip() new_v = os.getenv("SHAI_BOOT_NEW", "").strip()
rss = os.getenv("SHAI_REPO_RSS", "").strip()
line = None # Build status line
if status == "fetched_new": status_line = _build_status_line(status, old_v, new_v, desc)
line = f"Successfully fetched, cached, and booted new version: v{old_v or '0.0.0.0'} → v{new_v}" if not status_line:
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:
return # nothing to say 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) modlog_channel_id = cfg(bot).int('modlog_channel_id', 0)
if not modlog_channel_id: if not modlog_channel_id:
return return
# Find channel across guilds
ch = None ch = None
for g in bot.guilds: for g in bot.guilds:
ch = g.get_channel(modlog_channel_id) ch = g.get_channel(modlog_channel_id)
@ -61,8 +157,18 @@ async def post_boot_notice(bot):
if not ch: if not ch:
return return
# Post the status
try: try:
msg = line if not desc else f"{line}\n_{desc}_" await ch.send(status_line, allowed_mentions=discord.AllowedMentions.none())
await ch.send(msg, allowed_mentions=discord.AllowedMentions.none())
except Exception: 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