0.3.9.5.a4
Incorporated fault-tolerant startup messages with proper fetching of commit messages
This commit is contained in:
		
							parent
							
								
									0038a1889c
								
							
						
					
					
						commit
						b780c4069e
					
				
							
								
								
									
										2
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								bot.py
									
									
									
									
									
								
							@ -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; doesn’t trigger auto update)
 | 
					# Major.Enhancement.Minor.Patch.Test  (Test is alphanumeric; doesn’t trigger auto update)
 | 
				
			||||||
VERSION = "0.3.9.5.a3"
 | 
					VERSION = "0.3.9.5.a4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------- Env loading ----------
 | 
					# ---------- Env loading ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,16 @@
 | 
				
			|||||||
# modules/common/boot_notice.py
 | 
					# modules/common/boot_notice.py
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import base64
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
from datetime import datetime, timezone
 | 
					from datetime import datetime, timezone
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
import aiohttp
 | 
					import aiohttp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from modules.common.settings import cfg
 | 
					from modules.common.settings import cfg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------------- Version helpers ----------------
 | 
					# ---------- Version helpers ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_VERSION_RE = re.compile(r'\b\d+\.\d+\.\d+\.\d+(?:\.[A-Za-z0-9]+)?\b')
 | 
					_VERSION_RE = re.compile(r'\b\d+\.\d+\.\d+\.\d+(?:\.[A-Za-z0-9]+)?\b')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,7 +24,6 @@ def _split_subject_body(full_message: str) -> tuple[str | None, str | None]:
 | 
				
			|||||||
    if not full_message:
 | 
					    if not full_message:
 | 
				
			||||||
        return None, None
 | 
					        return None, None
 | 
				
			||||||
    lines = [ln.rstrip() for ln in full_message.splitlines()]
 | 
					    lines = [ln.rstrip() for ln in full_message.splitlines()]
 | 
				
			||||||
    # subject = first non-empty line
 | 
					 | 
				
			||||||
    subject = None
 | 
					    subject = None
 | 
				
			||||||
    i = 0
 | 
					    i = 0
 | 
				
			||||||
    while i < len(lines) and subject is None:
 | 
					    while i < len(lines) and subject is None:
 | 
				
			||||||
@ -37,19 +34,14 @@ def _split_subject_body(full_message: str) -> tuple[str | None, str | None]:
 | 
				
			|||||||
    return subject or None, (body or None)
 | 
					    return subject or None, (body or None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _cmp_versions(a: str | None, b: str | None) -> int:
 | 
					def _cmp_versions(a: str | None, b: str | None) -> int:
 | 
				
			||||||
    """
 | 
					    """Compare 1.2.3.4.a2 style; if either missing, treat as equal (0)."""
 | 
				
			||||||
    Compare versions like 1.2.3.4.a2 (last segment may be alnum).
 | 
					 | 
				
			||||||
    Returns -1 if a<b, 0 if equal/unknown, +1 if a>b.
 | 
					 | 
				
			||||||
    If either is None, treat as equal to avoid false positives.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if not a or not b:
 | 
					    if not a or not b:
 | 
				
			||||||
        return 0
 | 
					        return 0
 | 
				
			||||||
    pa = a.split('.')
 | 
					    pa, pb = a.split('.'), b.split('.')
 | 
				
			||||||
    pb = b.split('.')
 | 
					 | 
				
			||||||
    while len(pa) < 5: pa.append('0')
 | 
					    while len(pa) < 5: pa.append('0')
 | 
				
			||||||
    while len(pb) < 5: pb.append('0')
 | 
					    while len(pb) < 5: pb.append('0')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def part_key(x: str):
 | 
					    def key(x: str):
 | 
				
			||||||
        if x.isdigit():
 | 
					        if x.isdigit():
 | 
				
			||||||
            return (int(x), '', 1)
 | 
					            return (int(x), '', 1)
 | 
				
			||||||
        m = re.match(r'(\d+)(.*)', x)
 | 
					        m = re.match(r'(\d+)(.*)', x)
 | 
				
			||||||
@ -58,7 +50,7 @@ def _cmp_versions(a: str | None, b: str | None) -> int:
 | 
				
			|||||||
        return (0, x, 3)
 | 
					        return (0, x, 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for xa, xb in zip(pa, pb):
 | 
					    for xa, xb in zip(pa, pb):
 | 
				
			||||||
        ka, kb = part_key(xa), part_key(xb)
 | 
					        ka, kb = key(xa), key(xb)
 | 
				
			||||||
        if ka[0] != kb[0]:
 | 
					        if ka[0] != kb[0]:
 | 
				
			||||||
            return 1 if ka[0] > kb[0] else -1
 | 
					            return 1 if ka[0] > kb[0] else -1
 | 
				
			||||||
        if ka[2] != kb[2]:
 | 
					        if ka[2] != kb[2]:
 | 
				
			||||||
@ -67,7 +59,7 @@ def _cmp_versions(a: str | None, b: str | None) -> int:
 | 
				
			|||||||
            return 1 if ka[1] > kb[1] else -1
 | 
					            return 1 if ka[1] > kb[1] else -1
 | 
				
			||||||
    return 0
 | 
					    return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------------- Gitea helpers ----------------
 | 
					# ---------- Gitea helpers ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _parse_repo_url(repo_url: str) -> tuple[str | None, str | None, str | None]:
 | 
					def _parse_repo_url(repo_url: str) -> tuple[str | None, str | None, str | None]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@ -75,11 +67,10 @@ def _parse_repo_url(repo_url: str) -> tuple[str | None, str | None, str | None]:
 | 
				
			|||||||
    api_base = https://host/api/v1
 | 
					    api_base = https://host/api/v1
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        pr = urlparse(repo_url)
 | 
					        pr = urlparse(repo_url.strip().rstrip('/'))
 | 
				
			||||||
        parts = [p for p in pr.path.split('/') if p]
 | 
					        parts = [p for p in pr.path.split('/') if p]
 | 
				
			||||||
        if len(parts) >= 2:
 | 
					        if len(parts) >= 2:
 | 
				
			||||||
            owner = parts[0]
 | 
					            owner, repo = parts[0], parts[1]
 | 
				
			||||||
            repo = parts[1]
 | 
					 | 
				
			||||||
            if repo.endswith('.git'):
 | 
					            if repo.endswith('.git'):
 | 
				
			||||||
                repo = repo[:-4]
 | 
					                repo = repo[:-4]
 | 
				
			||||||
            api_base = f"{pr.scheme}://{pr.netloc}/api/v1"
 | 
					            api_base = f"{pr.scheme}://{pr.netloc}/api/v1"
 | 
				
			||||||
@ -88,68 +79,74 @@ def _parse_repo_url(repo_url: str) -> tuple[str | None, str | None, str | None]:
 | 
				
			|||||||
        pass
 | 
					        pass
 | 
				
			||||||
    return None, None, None
 | 
					    return None, None, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def _gitea_get_json(url: str, token: str | None, user: str | None, timeout_sec: int = 10):
 | 
					def _auth_headers_from_cfg(r):
 | 
				
			||||||
    headers = {}
 | 
					    """
 | 
				
			||||||
    if token and user:
 | 
					    Build Authorization header using SHAI_REPO_AHTOKEN (cfg: repo_ahtoken).
 | 
				
			||||||
        # Basic auth with user:token
 | 
					    The value may be raw; we prefix 'token ' if needed.
 | 
				
			||||||
        cred = base64.b64encode(f"{user}:{token}".encode()).decode()
 | 
					    Also supports SHAI_GITEA_TOKEN / SHAI_GITEA_USER as secondary.
 | 
				
			||||||
        headers['Authorization'] = f"Basic {cred}"
 | 
					    """
 | 
				
			||||||
    elif token:
 | 
					    ahtoken = r.get('repo_ahtoken', '').strip()  # SHAI_REPO_AHTOKEN
 | 
				
			||||||
        headers['Authorization'] = f"token {token}"
 | 
					    if ahtoken:
 | 
				
			||||||
 | 
					        if not ahtoken.lower().startswith('token '):
 | 
				
			||||||
 | 
					            ahtoken = f"token {ahtoken}"
 | 
				
			||||||
 | 
					        return {"Authorization": ahtoken}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Optional secondary envs for future private usage
 | 
				
			||||||
 | 
					    tok = os.getenv("SHAI_GITEA_TOKEN", "").strip()
 | 
				
			||||||
 | 
					    usr = os.getenv("SHAI_GITEA_USER", "").strip()
 | 
				
			||||||
 | 
					    if tok and usr:
 | 
				
			||||||
 | 
					        import base64
 | 
				
			||||||
 | 
					        b64 = base64.b64encode(f"{usr}:{tok}".encode()).decode()
 | 
				
			||||||
 | 
					        return {"Authorization": f"Basic {b64}"}
 | 
				
			||||||
 | 
					    if tok:
 | 
				
			||||||
 | 
					        return {"Authorization": f"token {tok}"}
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def _http_json(url: str, headers: dict, timeout_sec: int = 10):
 | 
				
			||||||
    timeout = aiohttp.ClientTimeout(total=timeout_sec)
 | 
					    timeout = aiohttp.ClientTimeout(total=timeout_sec)
 | 
				
			||||||
    async with aiohttp.ClientSession(timeout=timeout, headers=headers) as sess:
 | 
					    async with aiohttp.ClientSession(timeout=timeout, headers=headers or {}) as sess:
 | 
				
			||||||
        async with sess.get(url) as resp:
 | 
					        async with sess.get(url) as resp:
 | 
				
			||||||
 | 
					            ctype = resp.headers.get("Content-Type", "")
 | 
				
			||||||
            if resp.status != 200:
 | 
					            if resp.status != 200:
 | 
				
			||||||
                text = await resp.text()
 | 
					                text = await resp.text()
 | 
				
			||||||
                raise RuntimeError(f"Gitea GET {url} -> {resp.status}: {text[:200]}")
 | 
					                raise RuntimeError(f"Gitea GET {url} -> {resp.status} ({ctype}): {text[:200]}")
 | 
				
			||||||
            # Gitea returns either dict or list depending on endpoint
 | 
					 | 
				
			||||||
            ctype = resp.headers.get("Content-Type", "")
 | 
					 | 
				
			||||||
            if "application/json" not in ctype:
 | 
					            if "application/json" not in ctype:
 | 
				
			||||||
                text = await resp.text()
 | 
					                text = await resp.text()
 | 
				
			||||||
                raise RuntimeError(f"Gitea GET {url} non-JSON: {ctype} {text[:200]}")
 | 
					                raise RuntimeError(f"Gitea GET {url} non-JSON {ctype}: {text[:200]}")
 | 
				
			||||||
            return await resp.json()
 | 
					            return await resp.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def _fetch_latest_commit(api_base: str, owner: str, repo: str, branch: str,
 | 
					async def _fetch_latest_commit(api_base: str, owner: str, repo: str, branch: str | None,
 | 
				
			||||||
                               token: str | None, user: str | None) -> tuple[str | None, str | None, str | None]:
 | 
					                               headers: dict) -> tuple[str | None, str | None, str | None]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Returns (sha, subject, body) for the latest commit on branch using the
 | 
					    Returns (sha, subject, body) for the latest commit using the list-commits endpoint:
 | 
				
			||||||
    list-commits endpoint which includes the full commit message:
 | 
					    /api/v1/repos/{owner}/{repo}/commits?sha=main&stat=false&verification=false&files=false&limit=1
 | 
				
			||||||
      /api/v1/repos/{owner}/{repo}/commits?sha={branch}&limit=1&stat=false&verification=false&files=false
 | 
					    If branch is falsy, omits 'sha' and lets server default.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    url = (
 | 
					    params = {
 | 
				
			||||||
        f"{api_base}/repos/{owner}/{repo}/commits"
 | 
					        "stat": "false",
 | 
				
			||||||
        f"?sha={branch}&limit=1&stat=false&verification=false&files=false"
 | 
					        "verification": "false",
 | 
				
			||||||
    )
 | 
					        "files": "false",
 | 
				
			||||||
    data = await _gitea_get_json(url, token, user)
 | 
					        "limit": "1",
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if branch:
 | 
				
			||||||
 | 
					        params["sha"] = branch
 | 
				
			||||||
 | 
					    url = f"{api_base}/repos/{owner}/{repo}/commits?{urlencode(params)}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data = await _http_json(url, headers)
 | 
				
			||||||
    if not isinstance(data, list) or not data:
 | 
					    if not isinstance(data, list) or not data:
 | 
				
			||||||
        raise RuntimeError("Commits list empty or invalid.")
 | 
					        raise RuntimeError("Commits list empty or invalid")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    latest = data[0]
 | 
					    latest = data[0]
 | 
				
			||||||
    sha = latest.get("sha") or latest.get("id")
 | 
					    sha = latest.get("sha") or latest.get("id")
 | 
				
			||||||
    message = ""
 | 
					    message = ""
 | 
				
			||||||
    commit_obj = latest.get("commit") or {}
 | 
					    commit_obj = latest.get("commit") or {}
 | 
				
			||||||
    if isinstance(commit_obj, dict):
 | 
					    if isinstance(commit_obj, dict):
 | 
				
			||||||
        message = commit_obj.get("message") or ""
 | 
					        message = commit_obj.get("message") or ""
 | 
				
			||||||
    if not message:
 | 
					
 | 
				
			||||||
        # Extremely unlikely on this endpoint, but try secondary fetch
 | 
					    subject, body = _split_subject_body(message or "")
 | 
				
			||||||
        if sha:
 | 
					 | 
				
			||||||
            for endpoint in (
 | 
					 | 
				
			||||||
                f"{api_base}/repos/{owner}/{repo}/git/commits/{sha}",
 | 
					 | 
				
			||||||
                f"{api_base}/repos/{owner}/{repo}/commits/{sha}",
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    det = await _gitea_get_json(endpoint, token, user)
 | 
					 | 
				
			||||||
                    if isinstance(det, dict):
 | 
					 | 
				
			||||||
                        m = det.get("message") or det.get("commit", {}).get("message")
 | 
					 | 
				
			||||||
                        if m:
 | 
					 | 
				
			||||||
                            message = str(m)
 | 
					 | 
				
			||||||
                            break
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
    subject, body = _split_subject_body(message)
 | 
					 | 
				
			||||||
    return sha, (subject or ""), (body or "")
 | 
					    return sha, (subject or ""), (body or "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------------- Boot reason inference ----------------
 | 
					# ---------- Boot reason inference ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _is_near_scheduled(now_utc: datetime, hhmm_utc: str | None, window_min: int = 5) -> bool:
 | 
					def _is_near_scheduled(now_utc: datetime, hhmm_utc: str | None, window_min: int = 5) -> bool:
 | 
				
			||||||
    if not hhmm_utc:
 | 
					    if not hhmm_utc:
 | 
				
			||||||
@ -159,8 +156,7 @@ def _is_near_scheduled(now_utc: datetime, hhmm_utc: str | None, window_min: int
 | 
				
			|||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
    sched = now_utc.replace(hour=hh, minute=mm, second=0, microsecond=0)
 | 
					    sched = now_utc.replace(hour=hh, minute=mm, second=0, microsecond=0)
 | 
				
			||||||
    delta = abs((now_utc - sched).total_seconds())
 | 
					    return abs((now_utc - sched).total_seconds()) <= window_min * 60
 | 
				
			||||||
    return delta <= window_min * 60
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _format_status_line(kind: str, old_ver: str | None, new_ver: str | None) -> str:
 | 
					def _format_status_line(kind: str, old_ver: str | None, new_ver: str | None) -> str:
 | 
				
			||||||
    if kind == "updated":
 | 
					    if kind == "updated":
 | 
				
			||||||
@ -173,25 +169,20 @@ def _format_status_line(kind: str, old_ver: str | None, new_ver: str | None) ->
 | 
				
			|||||||
        return f"⚠️ Version rollback detected: **{old_ver or 'unknown'}** → **{new_ver or 'unknown'}**"
 | 
					        return f"⚠️ Version rollback detected: **{old_ver or 'unknown'}** → **{new_ver or 'unknown'}**"
 | 
				
			||||||
    return "🟢 Bot started"
 | 
					    return "🟢 Bot started"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ---------------- Main entry ----------------
 | 
					# ---------- Main entry ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def post_boot_notice(bot):
 | 
					async def post_boot_notice(bot):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Always post a boot status to the modlog channel.
 | 
					    Always posts a startup status + the latest commit message (Version + md body)
 | 
				
			||||||
      - Waits for ready.
 | 
					    to the modlog channel. Uses SHAI_REPO_URL and SHAI_REPO_AHTOKEN via cfg().
 | 
				
			||||||
      - Infers reason (updated/scheduled/manual/rollback).
 | 
					 | 
				
			||||||
      - Fetches latest commit (full message) via list-commits endpoint.
 | 
					 | 
				
			||||||
      - Posts status + commit message (Version bold + md details).
 | 
					 | 
				
			||||||
      - Pings guild owner only on rollback.
 | 
					 | 
				
			||||||
      - Persists last sha/version in data_manager['boot_state'].
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        await bot.wait_until_ready()
 | 
					        await bot.wait_until_ready()
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print(f"[boot_notice] wait_until_ready failed: {e}")
 | 
					        print(f"[boot_notice] wait_until_ready failed: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Resolve modlog channel
 | 
					    r = cfg(bot)
 | 
				
			||||||
    modlog_channel_id = cfg(bot).int('modlog_channel_id', 0)
 | 
					    modlog_channel_id = r.int('modlog_channel_id', 0)
 | 
				
			||||||
    if not modlog_channel_id:
 | 
					    if not modlog_channel_id:
 | 
				
			||||||
        print("[boot_notice] modlog_channel_id not configured; skipping.")
 | 
					        print("[boot_notice] modlog_channel_id not configured; skipping.")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
@ -206,37 +197,34 @@ async def post_boot_notice(bot):
 | 
				
			|||||||
        print(f"[boot_notice] channel id {modlog_channel_id} not found; skipping.")
 | 
					        print(f"[boot_notice] channel id {modlog_channel_id} not found; skipping.")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Repo info
 | 
					    repo_url = r.get('repo_url', '')                   # SHAI_REPO_URL
 | 
				
			||||||
    r = cfg(bot)
 | 
					    # Branch optional; if empty, we omit 'sha='
 | 
				
			||||||
    repo_url = r.get('repo_url', '')
 | 
					    branch = r.get('repo_branch', 'main') or None      # SHAI_REPO_BRANCH (optional)
 | 
				
			||||||
    branch = r.get('repo_branch', 'main')
 | 
					    check_time_utc = r.get('check_time_utc', '')       # SHAI_CHECK_TIME_UTC (optional, e.g. "03:00")
 | 
				
			||||||
    check_time_utc = r.get('check_time_utc', '')  # e.g., "03:00"
 | 
					    headers = _auth_headers_from_cfg(r)
 | 
				
			||||||
    now_utc = datetime.now(timezone.utc)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api_base = owner = repo = None
 | 
					    api_base = owner = repo = None
 | 
				
			||||||
    if repo_url:
 | 
					    if repo_url:
 | 
				
			||||||
        api_base, owner, repo = _parse_repo_url(repo_url)
 | 
					        api_base, owner, repo = _parse_repo_url(repo_url)
 | 
				
			||||||
 | 
					        if not all([api_base, owner, repo]):
 | 
				
			||||||
 | 
					            print(f"[boot_notice] failed to parse repo_url={repo_url!r}")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        print("[boot_notice] repo_url missing; commit lookup skipped.")
 | 
					        print("[boot_notice] repo_url missing; commit lookup skipped.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    token = os.getenv("SHAI_GITEA_TOKEN", "").strip() or None
 | 
					 | 
				
			||||||
    user  = os.getenv("SHAI_GITEA_USER", "").strip() or None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # State
 | 
					    # State
 | 
				
			||||||
    dm = getattr(bot, "data_manager", None)
 | 
					    dm = getattr(bot, "data_manager", None)
 | 
				
			||||||
    if not dm:
 | 
					    if not dm:
 | 
				
			||||||
        print("[boot_notice] data_manager missing on bot; cannot persist state.")
 | 
					        print("[boot_notice] data_manager missing on bot; cannot persist state.")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					 | 
				
			||||||
    prev = (dm.get('boot_state') or [{}])[-1] if dm.get('boot_state') else {}
 | 
					    prev = (dm.get('boot_state') or [{}])[-1] if dm.get('boot_state') else {}
 | 
				
			||||||
    prev_sha = prev.get('last_sha') or None
 | 
					    prev_sha = prev.get('last_sha') or None
 | 
				
			||||||
    prev_ver = prev.get('last_version') or None
 | 
					    prev_ver = prev.get('last_version') or None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Fetch latest commit (sha, subject, body)
 | 
					    # Fetch latest commit
 | 
				
			||||||
    sha = subject = body = None
 | 
					    sha = subject = body = None
 | 
				
			||||||
    if api_base and owner and repo:
 | 
					    if api_base and owner and repo:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            sha, subject, body = await _fetch_latest_commit(api_base, owner, repo, branch, token, user)
 | 
					            sha, subject, body = await _fetch_latest_commit(api_base, owner, repo, branch, headers)
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            print(f"[boot_notice] fetch latest commit failed: {e}")
 | 
					            print(f"[boot_notice] fetch latest commit failed: {e}")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
@ -245,34 +233,31 @@ async def post_boot_notice(bot):
 | 
				
			|||||||
    curr_ver = _extract_version(subject) if subject else None
 | 
					    curr_ver = _extract_version(subject) if subject else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Decide reason
 | 
					    # Decide reason
 | 
				
			||||||
    reason = "manual"
 | 
					    now_utc = datetime.now(timezone.utc)
 | 
				
			||||||
    mention_owner = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if prev_ver and curr_ver:
 | 
					    if prev_ver and curr_ver:
 | 
				
			||||||
        cmpv = _cmp_versions(prev_ver, curr_ver)
 | 
					        cmpv = _cmp_versions(prev_ver, curr_ver)
 | 
				
			||||||
        if cmpv < 0:
 | 
					        if cmpv < 0:
 | 
				
			||||||
            reason = "updated"
 | 
					            reason, ping_owner = "updated", False
 | 
				
			||||||
        elif cmpv > 0:
 | 
					        elif cmpv > 0:
 | 
				
			||||||
            reason = "rollback"
 | 
					            reason, ping_owner = "rollback", True
 | 
				
			||||||
            mention_owner = True
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            reason = "scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"
 | 
					            reason, ping_owner = ("scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"), False
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        if prev_sha and sha and prev_sha != sha:
 | 
					        if prev_sha and sha and prev_sha != sha:
 | 
				
			||||||
            reason = "updated"
 | 
					            reason, ping_owner = "updated", False
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            reason = "scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"
 | 
					            reason, ping_owner = ("scheduled" if _is_near_scheduled(now_utc, check_time_utc) else "manual"), False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Post status line
 | 
					    # Post status
 | 
				
			||||||
    status_line = _format_status_line(reason, prev_ver, curr_ver)
 | 
					    status_line = _format_status_line(reason, prev_ver, curr_ver)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        allowed = discord.AllowedMentions(
 | 
					        allowed = discord.AllowedMentions(
 | 
				
			||||||
            everyone=False,
 | 
					            everyone=False,
 | 
				
			||||||
            users=True if mention_owner else False,
 | 
					            users=True if (ping_owner and ch.guild and ch.guild.owner_id) else False,
 | 
				
			||||||
            roles=False,
 | 
					            roles=False,
 | 
				
			||||||
            replied_user=False
 | 
					            replied_user=False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if mention_owner and ch.guild and ch.guild.owner_id:
 | 
					        if ping_owner and ch.guild and ch.guild.owner_id:
 | 
				
			||||||
            status_line = f"{status_line}\n<@{ch.guild.owner_id}>"
 | 
					            status_line = f"{status_line}\n<@{ch.guild.owner_id}>"
 | 
				
			||||||
        await ch.send(status_line, allowed_mentions=allowed)
 | 
					        await ch.send(status_line, allowed_mentions=allowed)
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
@ -281,7 +266,7 @@ async def post_boot_notice(bot):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Post commit message (Version + md details)
 | 
					    # Post commit message (Version + md details)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        title = curr_ver or (subject or "Latest commit")
 | 
					        title = (curr_ver or subject or "Latest commit").strip()
 | 
				
			||||||
        if title or body:
 | 
					        if title or body:
 | 
				
			||||||
            commit_msg = f"**{title}**\n{body}" if body else f"**{title}**"
 | 
					            commit_msg = f"**{title}**\n{body}" if body else f"**{title}**"
 | 
				
			||||||
            await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none())
 | 
					            await ch.send(commit_msg, allowed_mentions=discord.AllowedMentions.none())
 | 
				
			||||||
@ -290,12 +275,11 @@ async def post_boot_notice(bot):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Persist state
 | 
					    # Persist state
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        new_state = {
 | 
					        dm.add('boot_state', {
 | 
				
			||||||
            'last_sha': sha,
 | 
					            'last_sha': sha,
 | 
				
			||||||
            'last_version': curr_ver,
 | 
					            'last_version': curr_ver,
 | 
				
			||||||
            'last_subject': subject,
 | 
					            'last_subject': subject,
 | 
				
			||||||
            'last_boot_ts': time.time(),
 | 
					            'last_boot_ts': time.time(),
 | 
				
			||||||
        }
 | 
					        })
 | 
				
			||||||
        dm.add('boot_state', new_state)
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        print(f"[boot_notice] failed to persist boot_state: {e}")
 | 
					        print(f"[boot_notice] failed to persist boot_state: {e}")
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user