1209 lines
66 KiB
Python
1209 lines
66 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FeedToRocket.py
|
|
|
|
Unified feeder to post Bugzilla, Koji, Wiki and Gitea events to Rocket.Chat webhooks.
|
|
|
|
Usage examples:
|
|
python FeedToRocket.py
|
|
python FeedToRocket.py --sleep 5 --log-level DEBUG
|
|
python FeedToRocket.py --one-off --feeds wiki,gitea
|
|
python FeedToRocket.py --empty-db
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
import socket
|
|
import time
|
|
import json
|
|
import re
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
import requests
|
|
import feedparser
|
|
import xml.etree.ElementTree as ET
|
|
from bs4 import BeautifulSoup
|
|
from dotenv import load_dotenv
|
|
|
|
# ---------------------------
|
|
# Load .env variables
|
|
# ---------------------------
|
|
load_dotenv()
|
|
|
|
# ---------------------------
|
|
# Configuration (from .env)
|
|
# ---------------------------
|
|
FEED_CONFIG = {
|
|
"bugzilla": {
|
|
"enabled": True,
|
|
"type": "bugzilla",
|
|
"domain": "bugs.koozali.org",
|
|
"feed_path": "/buglist.cgi?chfield=%5BBug%20creation%5D&chfieldfrom=7d&ctype=atom&title=Bugs%20reported%20in%20the%20last%207%20days",
|
|
"chat_url": os.getenv("TEST_CHAT_URL"),
|
|
#chat_url": os.getenv("BUGZILLA_CHAT_URL"),
|
|
"filter_field": "status",
|
|
"filter_value": "open",
|
|
"bypass_filter": True,
|
|
},
|
|
"koji": {
|
|
"enabled": True,
|
|
"type": "koji",
|
|
"domain": "koji.koozali.org",
|
|
"feed_path": "/koji/recentbuilds?feed=rss",
|
|
#"chat_url": os.getenv("KOJI_CHAT_URL")
|
|
"chat_url": os.getenv("TEST_CHAT_URL")
|
|
},
|
|
"wiki": {
|
|
"enabled": True,
|
|
"type": "wiki",
|
|
"domain": "wiki.koozali.org",
|
|
"feed_path": "/api.php?hidebots=1&urlversion=2&days=7&limit=50&action=feedrecentchanges&feedformat=rss",
|
|
"chat_url": os.getenv("TEST_CHAT_URL")
|
|
#"chat_url": os.getenv("WIKI_CHAT_URL")
|
|
}
|
|
}
|
|
|
|
GITEA_ORGS = ["smecontribs", "smeserver","smedev"]
|
|
for org in GITEA_ORGS:
|
|
FEED_CONFIG[f"gitea_{org}"] = {
|
|
"enabled": True,
|
|
"type": "gitea",
|
|
"feed_url": f"https://src.koozali.org/{org}.atom",
|
|
"org": org, # ✅ this line ensures process_gitea knows the org name
|
|
"chat_url": os.getenv("TEST_CHAT_URL"),
|
|
# "chat_url": os.getenv("GITEA_CHAT_URL")
|
|
}
|
|
|
|
# ---------------------------
|
|
# Logging configuration
|
|
# ---------------------------
|
|
DEFAULT_LOG_FILENAME = "FeedToRocket.log"
|
|
DEFAULT_LOG_DIR = "/var/log"
|
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
|
|
|
def init_logging(log_level_str: str) -> str:
|
|
log_level = getattr(logging, log_level_str.upper(), logging.INFO)
|
|
preferred_path = os.path.join(DEFAULT_LOG_DIR, DEFAULT_LOG_FILENAME)
|
|
fallback_path = os.path.join(".", DEFAULT_LOG_FILENAME)
|
|
log_file = preferred_path
|
|
try:
|
|
os.makedirs(os.path.dirname(preferred_path), exist_ok=True)
|
|
logging.basicConfig(filename=preferred_path, level=log_level, format=LOG_FORMAT)
|
|
logging.getLogger().info("Logging initialized at %s", preferred_path)
|
|
except PermissionError:
|
|
log_file = fallback_path
|
|
logging.basicConfig(filename=fallback_path, level=log_level, format=LOG_FORMAT)
|
|
logging.getLogger().info("Permission denied for %s. Logging initialized at %s", preferred_path, fallback_path)
|
|
console = logging.StreamHandler()
|
|
console.setLevel(log_level)
|
|
console.setFormatter(logging.Formatter(LOG_FORMAT))
|
|
logging.getLogger().addHandler(console)
|
|
return log_file
|
|
|
|
# ---------------------------
|
|
# Database helpers
|
|
# ---------------------------
|
|
DB_PATH = "sent_items.db"
|
|
|
|
def setup_database():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS sent_items (
|
|
feed_name TEXT,
|
|
item_id TEXT,
|
|
PRIMARY KEY(feed_name, item_id)
|
|
)
|
|
''')
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def clear_database():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute('DELETE FROM sent_items')
|
|
conn.commit()
|
|
conn.close()
|
|
logging.getLogger().info("Cleared the sent_items database")
|
|
|
|
def has_been_sent(feed_name: str, item_id: str) -> bool:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT 1 FROM sent_items WHERE feed_name = ? AND item_id = ?', (feed_name, item_id))
|
|
exists = cursor.fetchone() is not None
|
|
conn.close()
|
|
return exists
|
|
|
|
def mark_as_sent(feed_name: str, item_id: str):
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute('INSERT OR IGNORE INTO sent_items (feed_name, item_id) VALUES (?, ?)', (feed_name, item_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# ---------------------------
|
|
# Utilities
|
|
# ---------------------------
|
|
def get_ip_address(domain: str, retries: int = 10, delay: int = 1) -> str:
|
|
for attempt in range(1, retries + 1):
|
|
try:
|
|
ip_address = socket.gethostbyname(domain)
|
|
logging.info("Resolved %s -> %s", domain, ip_address)
|
|
return ip_address
|
|
except socket.gaierror:
|
|
logging.warning("Attempt %d failed to resolve %s, retrying...", attempt, domain)
|
|
time.sleep(delay)
|
|
raise RuntimeError(f"Unable to resolve domain '{domain}' after {retries} attempts.")
|
|
|
|
def send_to_rocket_chat(alias: str, text: str, attachments: Optional[List[Dict[str, Any]]], chat_url: str):
|
|
if not chat_url:
|
|
logging.warning("No chat URL for alias %s; skipping message.", alias)
|
|
return
|
|
payload = {"alias": alias, "text": text, "attachments": attachments or []}
|
|
try:
|
|
r = requests.post(chat_url, json=payload, timeout=10)
|
|
if r.status_code == 200:
|
|
logging.info("%s: message sent to Rocket.Chat", alias)
|
|
else:
|
|
logging.error("%s: Rocket.Chat returned %d - %s", alias, r.status_code, r.text)
|
|
except Exception as e:
|
|
logging.exception("%s: failed to send to Rocket.Chat: %s", alias, e)
|
|
|
|
def send_startup_message(feed_name: str, chat_url: str):
|
|
send_to_rocket_chat(feed_name.capitalize(), f"{feed_name.capitalize()} integration started successfully.", None, chat_url)
|
|
|
|
# ---------------------------
|
|
# Feed Processors
|
|
# ---------------------------
|
|
|
|
# Bugzilla
|
|
def parse_bugzilla_summary(summary: str):
|
|
summary = summary.replace("<", "<").replace(">", ">").replace("&", "&")
|
|
try:
|
|
root = ET.fromstring(summary)
|
|
except Exception as e:
|
|
logging.warning("Bugzilla parse error: %s", e)
|
|
return "", "", "", "", ""
|
|
status = reported_by = last_changed = product = component = ""
|
|
for row in root.findall('.//tr'):
|
|
if len(row) < 2: continue
|
|
field = (row[0].text or "").strip()
|
|
value = (row[1].text or "").strip()
|
|
if field == "Status": status = value
|
|
elif field == "ReportedByName": reported_by = value
|
|
elif field == "Last changed date": last_changed = value
|
|
elif field == "Product": product = value
|
|
elif field == "Component": component = value
|
|
return status, reported_by, last_changed, product, component
|
|
|
|
def process_bugzilla(conf, one_shot, domain_cache):
|
|
domain = conf["domain"]
|
|
if domain not in domain_cache:
|
|
domain_cache[domain] = get_ip_address(domain)
|
|
feed_url = f"http://{domain_cache[domain]}{conf['feed_path']}"
|
|
headers = {"Host": domain, "Referer": f"https://{domain}/"}
|
|
feed = feedparser.parse(requests.get(feed_url, headers=headers, timeout=10).content)
|
|
for entry in feed.entries:
|
|
summary = getattr(entry, "summary", "")
|
|
status, reported_by, last_changed, product, component = parse_bugzilla_summary(summary)
|
|
bug_id = str(entry.id).split("=")[-1]
|
|
status_key = status.lower() if status else "unknown"
|
|
if conf.get("bypass_filter") or (getattr(entry, conf.get("filter_field", ""), "").lower() == conf.get("filter_value", "")):
|
|
key = f"{bug_id}:{status_key}"
|
|
if not has_been_sent("bugzilla", key):
|
|
text = f"*Bug ID:* {bug_id} | *Status:* {status} | *Reporter:* {reported_by}\nProduct: {product}, Component: {component}, Last Changed: {last_changed}"
|
|
send_to_rocket_chat("Bugzilla", text, [{"title": entry.title, "title_link": entry.link, "color": "#764FA5"}], conf["chat_url"])
|
|
mark_as_sent("bugzilla", key)
|
|
|
|
# Koji
|
|
def extract_koji_changelog(link, host):
|
|
try:
|
|
r = requests.get(link, headers={"Host": host}, timeout=10)
|
|
soup = BeautifulSoup(r.text, "html.parser")
|
|
td = soup.find("td", class_="changelog")
|
|
if not td:
|
|
return ""
|
|
lines = [ln.strip() for ln in td.get_text().splitlines() if ln.strip()]
|
|
return "\n".join(lines[:5])
|
|
except Exception as e:
|
|
logging.warning("Koji changelog fetch failed: %s", e)
|
|
return ""
|
|
|
|
def process_koji(conf, one_shot, domain_cache):
|
|
domain = conf["domain"]
|
|
if domain not in domain_cache:
|
|
domain_cache[domain] = get_ip_address(domain)
|
|
url = f"http://{domain_cache[domain]}{conf['feed_path']}"
|
|
feed = feedparser.parse(requests.get(url, headers={"Host": domain}, timeout=10).content)
|
|
for entry in feed.entries:
|
|
title = entry.title.strip()
|
|
build_id = title.split(":")[1].split(",")[0].strip() if ":" in title else entry.id
|
|
if not has_been_sent("koji", build_id):
|
|
link = entry.link #.replace(domain, domain_cache[domain])
|
|
changelog = extract_koji_changelog(link, domain)
|
|
text = f"Build Notification - ID: {build_id}\n{changelog}"
|
|
send_to_rocket_chat("Koji", text, [{"title": title, "title_link": entry.link, "color": "#764FA5"}], conf["chat_url"])
|
|
mark_as_sent("koji", build_id)
|
|
|
|
# Wiki
|
|
def process_wiki(conf, one_shot, domain_cache):
|
|
domain = conf["domain"]
|
|
if domain not in domain_cache:
|
|
domain_cache[domain] = get_ip_address(domain)
|
|
url = f"http://{domain_cache[domain]}{conf['feed_path']}"
|
|
feed = feedparser.parse(requests.get(url, headers={"Host": domain}, timeout=10).content)
|
|
logging.debug(f"Wiki feed:{feed}")
|
|
for entry in feed.entries:
|
|
wiki_id = entry.id
|
|
if not has_been_sent("wiki", wiki_id):
|
|
text = f"*Title:* {entry.title}\n*Date:* {entry.published}\n*Author:* {entry.author}\nLink: {entry.link}"
|
|
send_to_rocket_chat("Wiki", text, [{"title": entry.title, "title_link": entry.link, "color": "#764FA5"}], conf["chat_url"])
|
|
mark_as_sent("wiki", wiki_id)
|
|
|
|
# Gitea (Atom feed)
|
|
# --- Gitea helpers (shared by processor + selftest) ---
|
|
|
|
def _gitea_clean_author(a: str) -> str:
|
|
if not a:
|
|
return ""
|
|
a = re.sub(r"\S*noreply\S*", "", a) # drop noreply
|
|
a = re.sub(r"mailto:\S+", "", a) # drop mailto
|
|
a = re.sub(r"<.*?>", "", a) # drop angle-bracketed emails
|
|
a = re.sub(r"\S+@\S+", "", a) # drop raw emails that might slip in
|
|
return a.strip()
|
|
|
|
def _gitea_text_only(html_snippet: str) -> str:
|
|
return BeautifulSoup(html_snippet or "", "html.parser").get_text(" ", strip=True)
|
|
|
|
def _gitea_first_two_links(html_snippet: str, base_host: str):
|
|
"""
|
|
Return (diff_link, diff_text, repo_link, repo_text) from an HTML snippet.
|
|
Only consider anchors pointing to src.koozali.org, make them absolute, ensure distinct.
|
|
"""
|
|
from urllib.parse import urljoin
|
|
diff_link = diff_text = repo_link = repo_text = None
|
|
soup = BeautifulSoup(html_snippet or "", "html.parser")
|
|
anchors = []
|
|
for a in soup.find_all("a"):
|
|
href = (a.get("href") or "").strip()
|
|
if "src.koozali.org" in href:
|
|
anchors.append((urljoin(base_host, href), a.get_text(strip=True)))
|
|
if anchors:
|
|
diff_link, diff_text = anchors[0]
|
|
if len(anchors) > 1:
|
|
repo_link, repo_text = anchors[-1]
|
|
else:
|
|
repo_link, repo_text = diff_link, diff_text
|
|
return diff_link, diff_text, repo_link, repo_text
|
|
|
|
def _gitea_build_attachment(org_name: str,
|
|
title_html: str,
|
|
summary_html: str,
|
|
content_html: str,
|
|
entry_link: str,
|
|
updated: str,
|
|
author_raw: str,
|
|
base_host: str) -> dict:
|
|
"""
|
|
Build a single Rocket.Chat attachment dict:
|
|
title: org_name
|
|
title_link: diff (or repo/entry_link)
|
|
text: compact body with Date, Author, and 'Diff | Repo' (each once)
|
|
"""
|
|
import html
|
|
from urllib.parse import urljoin
|
|
|
|
author = _gitea_clean_author(author_raw)
|
|
|
|
# Prefer links from <title>, then fallback to summary/content
|
|
diff_link, diff_text, repo_link, repo_text = _gitea_first_two_links(title_html, base_host)
|
|
if not diff_link or not repo_link:
|
|
alt_diff, alt_dt, alt_repo, alt_rt = _gitea_first_two_links(summary_html or content_html, base_host)
|
|
diff_link = diff_link or alt_diff
|
|
diff_text = diff_text or alt_dt
|
|
repo_link = repo_link or alt_repo
|
|
repo_text = repo_text or alt_rt
|
|
|
|
# If still no repo_link, try to derive from entry_link
|
|
if not repo_link and entry_link:
|
|
entry_link = urljoin(base_host, entry_link)
|
|
m = re.search(r"^(https?://[^/]+/[^/]+/[^/]+)", entry_link)
|
|
if m:
|
|
repo_link = m.group(1)
|
|
repo_text = "/".join(repo_link.rstrip("/").split("/")[-2:])
|
|
|
|
# Guard against any accidental email capture in repo_text
|
|
if repo_text and "@" in repo_text:
|
|
repo_text = None
|
|
if (not repo_text) and repo_link:
|
|
repo_text = "/".join(repo_link.rstrip("/").split("/")[-2:])
|
|
|
|
# Compact action line: clean readable text from <title>
|
|
title_text = html.unescape(_gitea_text_only(title_html))
|
|
action_line = f"{org_name}: {title_text}"
|
|
|
|
# Links shown once; both only if distinct
|
|
link_parts = []
|
|
if diff_link:
|
|
link_parts.append(f"[Diff]({diff_link})")
|
|
if repo_link and (repo_link != diff_link):
|
|
link_parts.append(f"[Repo]({repo_link})")
|
|
|
|
lines = [action_line]
|
|
if updated:
|
|
lines.append(f"Date: {updated}")
|
|
author = author.strip()
|
|
if author:
|
|
lines.append(f"Author: {author}")
|
|
if link_parts:
|
|
lines.append(" | ".join(link_parts))
|
|
|
|
text = "\n\n".join(lines)
|
|
title_link = diff_link or repo_link or urljoin(base_host, entry_link or "")
|
|
|
|
return {
|
|
"title": f"{org_name}",
|
|
"title_link": title_link,
|
|
"text": text,
|
|
"color": "#764FA5",
|
|
"collapsed": False
|
|
}
|
|
|
|
|
|
def process_gitea(conf, one_shot, domain_cache):
|
|
"""
|
|
Gitea Atom -> Rocket.Chat (compact, using attachment fields):
|
|
Fields shown:
|
|
- Action: <org>: <human-readable title>
|
|
- Message: <one-line commit message> (if available)
|
|
- Date: <updated> (if available)
|
|
- Author: <author> (if available)
|
|
- Links: [Diff](...) | [Repo](...) (Diff is compare/commit/branch/tag; Repo is repo home)
|
|
|
|
Diff priority: compare > commit > branch > tag
|
|
For tag-only entries, resolves tag -> commit (and previous tag) via Gitea API
|
|
to generate a commit or compare diff link.
|
|
|
|
Expects helpers elsewhere:
|
|
- has_been_sent(key, id)
|
|
- mark_as_sent(key, id)
|
|
- send_to_rocket_chat(username, text, attachments, webhook_url)
|
|
"""
|
|
import re
|
|
import html
|
|
import json
|
|
import logging
|
|
from urllib.parse import urljoin
|
|
|
|
import requests
|
|
import feedparser
|
|
from bs4 import BeautifulSoup
|
|
|
|
# ---- Constants / config --------------------------------------------------------
|
|
base_host = "https://src.koozali.org"
|
|
GITEA_API_BASE = f"{base_host}/api/v1"
|
|
|
|
feed_url = conf.get("feed_url")
|
|
chat_url = conf.get("chat_url")
|
|
org_name = conf.get("org") or (
|
|
re.search(r"src\.koozali\.org/([^/.]+)", conf.get("feed_url", "")) or [None]
|
|
)[1] or "unknown"
|
|
|
|
log = logging.getLogger(f"gitea.{org_name}")
|
|
|
|
if not feed_url or not chat_url:
|
|
log.warning("Skipping Gitea: feed URL or chat URL missing (org=%s)", org_name)
|
|
return
|
|
|
|
# ---- Helpers ------------------------------------------------------------------
|
|
|
|
def clean_author(a: str) -> str:
|
|
if not a:
|
|
return ""
|
|
a = re.sub(r"\S*noreply\S*", "", a) # drop noreply
|
|
a = re.sub(r"mailto:\S+", "", a) # drop mailto
|
|
a = re.sub(r"<.*?>", "", a) # drop angle-brackets
|
|
a = re.sub(r"\S+@\S+", "", a) # drop raw emails
|
|
return a.strip()
|
|
|
|
def text_only(html_snippet: str) -> str:
|
|
return html.unescape(BeautifulSoup(html_snippet or "", "html.parser").get_text(" ", strip=True))
|
|
|
|
def _sanitize_url(u: str) -> str:
|
|
u = (u or "").strip()
|
|
# Trim artifacts from scraping
|
|
u = re.sub(r'[">\')\]]+$', '', u) # closing quotes/brackets/parens
|
|
u = re.sub(r'[.,;:]+$', '', u) # trailing punctuation
|
|
return u
|
|
|
|
def _normalize_url(u: str) -> str:
|
|
return urljoin(base_host, _sanitize_url(u))
|
|
|
|
def _collect_links(entry):
|
|
"""
|
|
Return absolute src.koozali.org URLs with texts from:
|
|
- title (anchors)
|
|
- summary or content (anchors)
|
|
- entry.link (raw)
|
|
- entry.id (raw; after colon)
|
|
- PLUS: any raw URLs in summary/content (non-anchors) inc. relative paths
|
|
De-dupes by URL.
|
|
"""
|
|
links = []
|
|
|
|
def add_from_anchors(snippet):
|
|
if not snippet:
|
|
return
|
|
soup = BeautifulSoup(snippet, "html.parser")
|
|
for a in soup.find_all("a"):
|
|
href = _sanitize_url(a.get("href") or "")
|
|
if "src.koozali.org" in href:
|
|
links.append((_normalize_url(href), a.get_text(strip=True)))
|
|
|
|
def add_from_raw(text):
|
|
if not text:
|
|
return
|
|
# absolute URLs
|
|
for m in re.finditer(r"https?://src\.koozali\.org[^\s)>\]\"']+", text):
|
|
links.append((_normalize_url(m.group(0)), ""))
|
|
# relative (common org prefixes or any /compare/)
|
|
for m in re.finditer(r"(?:^|\s)(/[^ \t\n\r)>\]\"']+)", text):
|
|
candidate = m.group(1)
|
|
if candidate.startswith(("/smeserver/", "/smecontribs/", "/common/")) or "/compare/" in candidate:
|
|
links.append((_normalize_url(candidate), ""))
|
|
|
|
title_html = getattr(entry, "title", "")
|
|
summary_html = getattr(entry, "summary", "")
|
|
content_html = ""
|
|
if getattr(entry, "content", None):
|
|
content_html = entry.content[0].get("value", "")
|
|
|
|
# Anchors
|
|
add_from_anchors(title_html)
|
|
add_from_anchors(summary_html or content_html)
|
|
|
|
# entry.link
|
|
elink = getattr(entry, "link", "")
|
|
if "src.koozali.org" in elink:
|
|
links.append((_normalize_url(elink), ""))
|
|
|
|
# URL inside id (e.g., "123: https://...")
|
|
eid = getattr(entry, "id", "")
|
|
m = re.search(r"https?://[^ \t\n\r]+", eid)
|
|
if m and "src.koozali.org" in m.group(0):
|
|
links.append((_normalize_url(m.group(0)), ""))
|
|
|
|
# Raw text scan
|
|
add_from_raw(summary_html)
|
|
add_from_raw(content_html)
|
|
|
|
# De-dupe by URL
|
|
seen = set()
|
|
out = []
|
|
for u, t in links:
|
|
if "src.koozali.org" not in u:
|
|
continue
|
|
if u not in seen:
|
|
seen.add(u)
|
|
out.append((u, t))
|
|
return out
|
|
|
|
def _classify(links):
|
|
"""Split into compare/commit/branch/tag and detect repo homes."""
|
|
compares, commits, branches, tags, repos = [], [], [], [], []
|
|
for url, txt in links:
|
|
if "/compare/" in url:
|
|
compares.append((url, txt or "compare"))
|
|
elif "/commit/" in url:
|
|
sha = url.rsplit("/", 1)[-1]
|
|
commits.append((url, txt or (sha[:7] if sha else "commit")))
|
|
elif "/src/branch/" in url:
|
|
br = url.rsplit("/", 1)[-1]
|
|
branches.append((url, txt or br))
|
|
elif "/src/tag/" in url:
|
|
tg = url.rsplit("/", 1)[-1]
|
|
tags.append((url, txt or tg))
|
|
|
|
# Repo home: https://host/org/repo
|
|
m = re.match(r"^https?://[^/]+/[^/]+/[^/]+/?$", url)
|
|
if m:
|
|
home = m.group(0).rstrip("/")
|
|
repos.append((home, "/".join(home.split("/")[-2:])))
|
|
return compares, commits, branches, tags, repos
|
|
|
|
def _extract_commit_message(summary_html: str, content_html: str) -> str:
|
|
"""
|
|
Pull a compact one-line commit message from summary/content.
|
|
- Convert to text with newlines preserved
|
|
- Drop a leading line that is just a SHA
|
|
- Return first meaningful non-empty line (trimmed to ~200 chars)
|
|
"""
|
|
soup = BeautifulSoup((summary_html or content_html) or "", "html.parser")
|
|
txt = soup.get_text("\n", strip=True)
|
|
if not txt:
|
|
return ""
|
|
lines = [ln.strip() for ln in txt.splitlines()]
|
|
if not lines:
|
|
return ""
|
|
|
|
sha_like = re.compile(r"^[0-9a-f]{7,40}$", re.I)
|
|
if lines and sha_like.match(lines[0]):
|
|
lines = lines[1:]
|
|
|
|
for ln in lines:
|
|
if not ln:
|
|
continue
|
|
# Skip pure mailto lines
|
|
if "mailto:" in ln.lower():
|
|
continue
|
|
# Compact whitespace and trim length
|
|
msg = re.sub(r"\s+", " ", ln).strip()
|
|
if msg:
|
|
return (msg[:200] + "…") if len(msg) > 200 else msg
|
|
return ""
|
|
|
|
# ---- Gitea API helpers (for tag-only upgrade) ---------------------------------
|
|
|
|
def _gitea_api_get_tags(owner: str, repo: str, timeout=10):
|
|
"""
|
|
Return list of tags (dicts) from Gitea:
|
|
[{"name": "v1.2.3", "commit": {"sha": "...", "url": "..."}}, ...]
|
|
On error, return [].
|
|
"""
|
|
url = f"{GITEA_API_BASE}/repos/{owner}/{repo}/tags"
|
|
try:
|
|
r = requests.get(url, timeout=timeout)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if isinstance(data, list):
|
|
return data
|
|
except Exception as e:
|
|
log.warning("Gitea API tags fetch failed for %s/%s: %s", owner, repo, e)
|
|
return []
|
|
|
|
def _resolve_tag_to_commit(owner: str, repo: str, tag_name: str, cache: dict):
|
|
"""
|
|
Map tag_name -> commit SHA via cache/api:
|
|
cache['owner/repo'][tag_name] = sha
|
|
"""
|
|
repo_key = f"{owner}/{repo}"
|
|
cache.setdefault(repo_key, {})
|
|
if tag_name in cache[repo_key]:
|
|
return cache[repo_key][tag_name]
|
|
tags = _gitea_api_get_tags(owner, repo)
|
|
for t in tags:
|
|
if t.get("name") == tag_name:
|
|
sha = (t.get("commit") or {}).get("sha")
|
|
if sha:
|
|
cache[repo_key][tag_name] = sha
|
|
return sha
|
|
return None
|
|
|
|
def _find_previous_tag(owner: str, repo: str, tag_name: str):
|
|
"""
|
|
Best-effort previous tag:
|
|
- Use /tags list order (typically newest-first). If current at i, pick i+1 if exists.
|
|
- Else first different tag.
|
|
"""
|
|
tags = _gitea_api_get_tags(owner, repo)
|
|
names = [t.get("name") for t in tags if t.get("name")]
|
|
if not names:
|
|
return None
|
|
try:
|
|
i = names.index(tag_name)
|
|
if i + 1 < len(names):
|
|
return names[i + 1]
|
|
except ValueError:
|
|
pass
|
|
for n in names:
|
|
if n != tag_name:
|
|
return n
|
|
return None
|
|
|
|
# ---- Fetch + parse feed -------------------------------------------------------
|
|
|
|
try:
|
|
log.info("Fetching Atom from %s", feed_url)
|
|
resp = requests.get(feed_url, timeout=15)
|
|
resp.raise_for_status()
|
|
feed = feedparser.parse(resp.content)
|
|
except Exception as e:
|
|
log.warning("Fetch/parse failed: %s", e)
|
|
return
|
|
|
|
sent_key = f"gitea_{org_name}"
|
|
|
|
# Cache for tag->sha resolution across entries
|
|
tag_sha_cache = domain_cache.setdefault("gitea_tag_sha_cache", {})
|
|
|
|
for entry in feed.entries:
|
|
try:
|
|
entry_id = getattr(entry, "id", None)
|
|
if not entry_id:
|
|
continue
|
|
if has_been_sent(sent_key, entry_id):
|
|
continue
|
|
|
|
title_html = getattr(entry, "title", "")
|
|
summary_html = getattr(entry, "summary", "")
|
|
content_html = ""
|
|
if getattr(entry, "content", None):
|
|
content_html = entry.content[0].get("value", "")
|
|
updated = getattr(entry, "updated", "")
|
|
author_raw = getattr(entry, "author", "")
|
|
|
|
# Collect + classify URLs
|
|
all_links = _collect_links(entry)
|
|
compares, commits, branches, tags, repos = _classify(all_links)
|
|
|
|
log.debug(
|
|
"Entry id=%s title='%s' links: compares=%s commits=%s branches=%s tags=%s repos=%s",
|
|
entry_id,
|
|
text_only(title_html)[:120],
|
|
[u for u, _ in compares],
|
|
[u for u, _ in commits],
|
|
[u for u, _ in branches],
|
|
[u for u, _ in tags],
|
|
[u for u, _ in repos],
|
|
)
|
|
|
|
# Repo: explicit home if present, else derive from any URL
|
|
if repos:
|
|
repo_link, _repo_text = repos[0]
|
|
else:
|
|
repo_link = None
|
|
for url, _ in (compares + commits + branches + tags):
|
|
m = re.match(r"^(https?://[^/]+/[^/]+/[^/]+)", url)
|
|
if m:
|
|
repo_link = m.group(1)
|
|
break
|
|
|
|
# Diff: compare > commit > branch > tag
|
|
diff_link = None
|
|
diff_kind = "-"
|
|
if compares:
|
|
diff_link, _ = compares[0]
|
|
diff_kind = "compare"
|
|
elif commits:
|
|
diff_link, _ = commits[0]
|
|
diff_kind = "commit"
|
|
elif branches:
|
|
diff_link, _ = branches[0]
|
|
diff_kind = "branch"
|
|
elif tags:
|
|
diff_link, _ = tags[0]
|
|
diff_kind = "tag"
|
|
|
|
# Tag-only upgrade: try to turn tag page into commit or compare
|
|
if diff_kind == "tag" and diff_link:
|
|
try:
|
|
m = re.match(r"^https?://[^/]+/([^/]+)/([^/]+)/src/tag/([^/]+)$", diff_link)
|
|
if m:
|
|
owner, repo, tag_name = m.group(1), m.group(2), m.group(3)
|
|
sha = _resolve_tag_to_commit(owner, repo, tag_name, tag_sha_cache)
|
|
if sha:
|
|
prev_tag = _find_previous_tag(owner, repo, tag_name)
|
|
if prev_tag:
|
|
diff_link = f"{base_host}/{owner}/{repo}/compare/{prev_tag}...{tag_name}"
|
|
diff_kind = "compare"
|
|
else:
|
|
diff_link = f"{base_host}/{owner}/{repo}/commit/{sha}"
|
|
diff_kind = "commit"
|
|
log.debug("Upgraded tag-only diff to %s: %s", diff_kind, diff_link)
|
|
else:
|
|
log.debug("Could not resolve tag '%s' for %s/%s; keeping tag URL", tag_name, owner, repo)
|
|
except Exception:
|
|
log.exception("Error upgrading tag-only entry to a commit/compare diff")
|
|
|
|
# Clean author & title; extract commit message
|
|
author = clean_author(author_raw)
|
|
title_text = text_only(title_html)
|
|
commit_msg = _extract_commit_message(summary_html, content_html)
|
|
|
|
# ---- Build fields -----------------------------------------------------
|
|
fields = []
|
|
fields.append({"title": "*Action*", "value": f"{org_name}: {title_text}", "short": False})
|
|
if commit_msg:
|
|
fields.append({"title": "*Message*", "value": commit_msg, "short": False})
|
|
if updated:
|
|
fields.append({"title": "*Date*", "value": updated, "short": True})
|
|
if author:
|
|
fields.append({"title": "*Author*", "value": author, "short": True})
|
|
|
|
link_parts = []
|
|
if diff_link:
|
|
link_parts.append(f"[Diff]({diff_link})")
|
|
if repo_link and (repo_link != diff_link):
|
|
link_parts.append(f"[Repo]({repo_link})")
|
|
if link_parts:
|
|
fields.append({"title": "*Links*", "value": " | ".join(link_parts), "short": False})
|
|
|
|
# Title click-through prefers diff, else repo
|
|
title_link = diff_link or repo_link
|
|
|
|
attachment = {
|
|
"title": f"{org_name}/{repo}",
|
|
"title_link": title_link,
|
|
"text": "", # we use fields for layout
|
|
"fields": fields, # <<<<<<<<<<<<<<<<<<<<<<<<
|
|
"color": "#764FA5",
|
|
"collapsed": False,
|
|
}
|
|
|
|
# Send & mark
|
|
send_to_rocket_chat(f"Gitea-{org_name}", "", [attachment], chat_url)
|
|
mark_as_sent(sent_key, entry_id)
|
|
log.info(
|
|
"Sent: diffKind=%s diff=%s repo=%s title='%s'",
|
|
diff_kind, (diff_link or "-"), (repo_link or "-"),
|
|
title_text[:160],
|
|
)
|
|
|
|
except Exception:
|
|
log.exception("Error processing Gitea entry for org '%s'", org_name)
|
|
|
|
|
|
# --- Self-test: runs against embedded sample Atom and prints attachments (no sending) ---
|
|
|
|
SMESERVER_SAMPLE_ATOM = r"""<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
|
<title>Feed of "smeserver"</title>
|
|
<id>https://src.koozali.org/smeserver</id>
|
|
<updated>2025-10-29T12:41:45+01:00</updated>
|
|
<link href="https://src.koozali.org/smeserver"></link>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-124_el8_sme">11_0_0-124_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-24T13:41:59+02:00</updated>
|
|
<id>84593: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-124_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-124_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-24T13:41:58+02:00</updated>
|
|
<id>84586: https://src.koozali.org/smeserver/smeserver-manager/commit/8e270ef3fd973ef27d0087fcfa02f614c1e13676</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/8e270ef3fd973ef27d0087fcfa02f614c1e13676" rel="nofollow">8e270ef3fd973ef27d0087fcfa02f614c1e13676</a>
* Fri Oct 24 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-124.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/8e270ef3fd973ef27d0087fcfa02f614c1e13676" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/8e270ef3fd973ef27d0087fcfa02f614c1e13676">8e270ef3fd973ef27d0087fcfa02f614c1e13676</a>
* Fri Oct 24 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-124.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-123_el8_sme">11_0_0-123_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-24T11:50:58+02:00</updated>
|
|
<id>84579: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-123_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-123_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-24T11:50:56+02:00</updated>
|
|
<id>84572: https://src.koozali.org/smeserver/smeserver-manager/commit/a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad" rel="nofollow">a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad</a>
* Fri Oct 24 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-123.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad">a04097bf5a2fefe78aa3d324bf4d9d9ce90f31ad</a>
* Fri Oct 24 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-123.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/src/tag/11_0_0-11_el8_sme">11_0_0-11_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery">smeserver/smeserver-manager-jsquery</a></title>
|
|
<updated>2025-10-24T11:41:20+02:00</updated>
|
|
<id>84565: https://src.koozali.org/smeserver/smeserver-manager-jsquery/src/tag/11_0_0-11_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/src/tag/11_0_0-11_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery">smeserver/smeserver-manager-jsquery</a></title>
|
|
<updated>2025-10-24T11:41:20+02:00</updated>
|
|
<id>84558: https://src.koozali.org/smeserver/smeserver-manager-jsquery/commit/e026aa17369953a482be3ecb1338f39cada6d03a</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/commit/e026aa17369953a482be3ecb1338f39cada6d03a" rel="nofollow">e026aa17369953a482be3ecb1338f39cada6d03a</a>
* Thu Oct 23 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-11.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/commit/e026aa17369953a482be3ecb1338f39cada6d03a" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager-jsquery/commit/e026aa17369953a482be3ecb1338f39cada6d03a">e026aa17369953a482be3ecb1338f39cada6d03a</a>
* Thu Oct 23 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-11.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/common/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/common">smeserver/common</a></title>
|
|
<updated>2025-10-23T18:20:23+02:00</updated>
|
|
<id>83886: /smeserver/common/compare/507cc753ec53612b622047344a527314158808a3...8d5535b58b89c2ab8757842325a687c73022f317</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/common/commit/8d5535b58b89c2ab8757842325a687c73022f317" rel="nofollow">8d5535b58b89c2ab8757842325a687c73022f317</a>
filter ARCHIVEFILE to only get archives ending with z

<a href="https://src.koozali.org/smeserver/common/commit/63f19e99973fe9d254394b3fbd08a57f14c8d7f4" rel="nofollow">63f19e99973fe9d254394b3fbd08a57f14c8d7f4</a>
add info</content>
|
|
<link href="/smeserver/common/compare/507cc753ec53612b622047344a527314158808a3...8d5535b58b89c2ab8757842325a687c73022f317" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/common/commit/8d5535b58b89c2ab8757842325a687c73022f317">8d5535b58b89c2ab8757842325a687c73022f317</a>
filter ARCHIVEFILE to only get archives ending with z

<a href="https://src.koozali.org/smeserver/common/commit/63f19e99973fe9d254394b3fbd08a57f14c8d7f4">63f19e99973fe9d254394b3fbd08a57f14c8d7f4</a>
add info</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-22T10:44:34+02:00</updated>
|
|
<id>83707: https://src.koozali.org/smeserver/smeserver-manager/commit/9437dd792a2117ba41c433881fef4a915acfcc2c</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/9437dd792a2117ba41c433881fef4a915acfcc2c" rel="nofollow">9437dd792a2117ba41c433881fef4a915acfcc2c</a>
html comment closure leaks onto panel</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/9437dd792a2117ba41c433881fef4a915acfcc2c" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/9437dd792a2117ba41c433881fef4a915acfcc2c">9437dd792a2117ba41c433881fef4a915acfcc2c</a>
html comment closure leaks onto panel</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-122_el8_sme">11_0_0-122_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-21T20:28:14+02:00</updated>
|
|
<id>83700: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-122_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-122_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-10-21T20:28:12+02:00</updated>
|
|
<id>83693: https://src.koozali.org/smeserver/smeserver-manager/commit/f03d82ebf746e38a7678f1ee82ab754bb20da9eb</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/f03d82ebf746e38a7678f1ee82ab754bb20da9eb" rel="nofollow">f03d82ebf746e38a7678f1ee82ab754bb20da9eb</a>
* Tue Oct 21 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-122.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/f03d82ebf746e38a7678f1ee82ab754bb20da9eb" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/f03d82ebf746e38a7678f1ee82ab754bb20da9eb">f03d82ebf746e38a7678f1ee82ab754bb20da9eb</a>
* Tue Oct 21 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-122.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jcrisp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-certificates/src/tag/11_0-11_el8_sme">11_0-11_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-certificates">smeserver/smeserver-certificates</a></title>
|
|
<updated>2025-10-15T17:10:17+02:00</updated>
|
|
<id>83634: https://src.koozali.org/smeserver/smeserver-certificates/src/tag/11_0-11_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-certificates/src/tag/11_0-11_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jcrisp</name>
|
|
<email>jcrisp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jcrisp pushed to <a href="https://src.koozali.org/smeserver/smeserver-certificates/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-certificates">smeserver/smeserver-certificates</a></title>
|
|
<updated>2025-10-15T17:05:40+02:00</updated>
|
|
<id>83627: https://src.koozali.org/smeserver/smeserver-certificates/commit/73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-certificates/commit/73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4" rel="nofollow">73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4</a>
Fix typo</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-certificates/commit/73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-certificates/commit/73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4">73ef48ef5f0bfb5e9e990b4f6b0948f662a3f9b4</a>
Fix typo</summary>
|
|
<author>
|
|
<name>jcrisp</name>
|
|
<email>jcrisp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-31_el8_sme">11_0_0-31_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-06T19:34:20+02:00</updated>
|
|
<id>81653: https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-31_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-31_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-update/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-06T19:34:15+02:00</updated>
|
|
<id>81646: https://src.koozali.org/smeserver/smeserver-update/commit/27485c3952d2aa55de468a70cc5f69939580bb87</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/27485c3952d2aa55de468a70cc5f69939580bb87" rel="nofollow">27485c3952d2aa55de468a70cc5f69939580bb87</a>
* Mon Oct 06 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-31.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/commit/27485c3952d2aa55de468a70cc5f69939580bb87" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/27485c3952d2aa55de468a70cc5f69939580bb87">27485c3952d2aa55de468a70cc5f69939580bb87</a>
* Mon Oct 06 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-31.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/smeserver-ntp/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-ntp">smeserver/smeserver-ntp</a></title>
|
|
<updated>2025-10-03T21:50:22+02:00</updated>
|
|
<id>81202: https://src.koozali.org/smeserver/smeserver-ntp/commit/8879d29ca50e750e0890f11d4c0d405954ae7c2e</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-ntp/commit/8879d29ca50e750e0890f11d4c0d405954ae7c2e" rel="nofollow">8879d29ca50e750e0890f11d4c0d405954ae7c2e</a>
typo in changelog</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-ntp/commit/8879d29ca50e750e0890f11d4c0d405954ae7c2e" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-ntp/commit/8879d29ca50e750e0890f11d4c0d405954ae7c2e">8879d29ca50e750e0890f11d4c0d405954ae7c2e</a>
typo in changelog</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-ntp/src/tag/11_0_0-8_el8_sme">11_0_0-8_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-ntp">smeserver/smeserver-ntp</a></title>
|
|
<updated>2025-10-03T21:48:25+02:00</updated>
|
|
<id>81195: https://src.koozali.org/smeserver/smeserver-ntp/src/tag/11_0_0-8_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-ntp/src/tag/11_0_0-8_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/smeserver-ntp/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-ntp">smeserver/smeserver-ntp</a></title>
|
|
<updated>2025-10-03T21:47:56+02:00</updated>
|
|
<id>81188: https://src.koozali.org/smeserver/smeserver-ntp/commit/6d07479bf668b0f0be4ae705841b3208fa9ee233</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-ntp/commit/6d07479bf668b0f0be4ae705841b3208fa9ee233" rel="nofollow">6d07479bf668b0f0be4ae705841b3208fa9ee233</a>
* Fri Oct 03 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="" rel="nofollow">jpp@koozali.org</a>&gt; 11.0.0-8.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-ntp/commit/6d07479bf668b0f0be4ae705841b3208fa9ee233" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-ntp/commit/6d07479bf668b0f0be4ae705841b3208fa9ee233">6d07479bf668b0f0be4ae705841b3208fa9ee233</a>
* Fri Oct 03 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="">jpp@koozali.org</a>&gt; 11.0.0-8.sme</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-30_el8_sme">11_0_0-30_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-03T15:17:43+02:00</updated>
|
|
<id>81125: https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-30_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-30_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/smeserver-update/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-03T15:17:15+02:00</updated>
|
|
<id>81118: https://src.koozali.org/smeserver/smeserver-update/commit/8e29af1670c27e91ff9846f7c677b8e1e943e94a</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/8e29af1670c27e91ff9846f7c677b8e1e943e94a" rel="nofollow">8e29af1670c27e91ff9846f7c677b8e1e943e94a</a>
* Thu Oct 02 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="" rel="nofollow">jpp@koozali.org</a>&gt; 11.0.0-30.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/commit/8e29af1670c27e91ff9846f7c677b8e1e943e94a" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/8e29af1670c27e91ff9846f7c677b8e1e943e94a">8e29af1670c27e91ff9846f7c677b8e1e943e94a</a>
* Thu Oct 02 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="">jpp@koozali.org</a>&gt; 11.0.0-30.sme</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-29_el8_sme">11_0_0-29_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-02T15:51:27+02:00</updated>
|
|
<id>81083: https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-29_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/src/tag/11_0_0-29_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/smeserver-update/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-update">smeserver/smeserver-update</a></title>
|
|
<updated>2025-10-02T15:49:58+02:00</updated>
|
|
<id>81076: https://src.koozali.org/smeserver/smeserver-update/commit/37f6399569c303a9a64de84bd39b836b1de78598</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/37f6399569c303a9a64de84bd39b836b1de78598" rel="nofollow">37f6399569c303a9a64de84bd39b836b1de78598</a>
* Thu Oct 02 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="" rel="nofollow">jpp@koozali.org</a>&gt; 11.0.0-29.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-update/commit/37f6399569c303a9a64de84bd39b836b1de78598" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-update/commit/37f6399569c303a9a64de84bd39b836b1de78598">37f6399569c303a9a64de84bd39b836b1de78598</a>
* Thu Oct 02 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="">jpp@koozali.org</a>&gt; 11.0.0-29.sme</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-121_el8_sme">11_0_0-121_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-27T13:30:07+02:00</updated>
|
|
<id>80913: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-121_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-121_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-27T13:30:05+02:00</updated>
|
|
<id>80906: https://src.koozali.org/smeserver/smeserver-manager/commit/de2f78a0892214b8c1fdd2de7b2eed177d398aac</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/de2f78a0892214b8c1fdd2de7b2eed177d398aac" rel="nofollow">de2f78a0892214b8c1fdd2de7b2eed177d398aac</a>
* Sat Sep 27 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-121.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/de2f78a0892214b8c1fdd2de7b2eed177d398aac" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/de2f78a0892214b8c1fdd2de7b2eed177d398aac">de2f78a0892214b8c1fdd2de7b2eed177d398aac</a>
* Sat Sep 27 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-121.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-12_el8_sme">11_0_0-12_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-proftpd">smeserver/smeserver-proftpd</a></title>
|
|
<updated>2025-09-26T18:49:52+02:00</updated>
|
|
<id>80885: https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-12_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-12_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed to <a href="https://src.koozali.org/smeserver/smeserver-proftpd/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-proftpd">smeserver/smeserver-proftpd</a></title>
|
|
<updated>2025-09-26T18:49:43+02:00</updated>
|
|
<id>80878: https://src.koozali.org/smeserver/smeserver-proftpd/commit/ed837ffb760943d12a6462c35c7c9f48176b91d8</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-proftpd/commit/ed837ffb760943d12a6462c35c7c9f48176b91d8" rel="nofollow">ed837ffb760943d12a6462c35c7c9f48176b91d8</a>
* Fri Sep 26 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="" rel="nofollow">jpp@koozali.org</a>&gt; 11.0.0-12.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-proftpd/commit/ed837ffb760943d12a6462c35c7c9f48176b91d8" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-proftpd/commit/ed837ffb760943d12a6462c35c7c9f48176b91d8">ed837ffb760943d12a6462c35c7c9f48176b91d8</a>
* Fri Sep 26 2025 Jean-Philippe Pialasse &lt;<a href="mailto:jpp@koozali.org" data-markdown-generated-content="">jpp@koozali.org</a>&gt; 11.0.0-12.sme</summary>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-120_el8_sme">11_0_0-120_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-25T19:45:32+02:00</updated>
|
|
<id>80752: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-120_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-120_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-25T19:45:30+02:00</updated>
|
|
<id>80745: https://src.koozali.org/smeserver/smeserver-manager/commit/b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7" rel="nofollow">b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7</a>
* Thu Sep 25 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-120.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7">b838d9252a7d6fd52e9dc3e9c6fc91f4f8d726d7</a>
* Thu Sep 25 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-120.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed tag <a href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-119_el8_sme">11_0_0-119_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-25T16:42:44+02:00</updated>
|
|
<id>80708: https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-119_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/src/tag/11_0_0-119_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>brianr pushed to <a href="https://src.koozali.org/smeserver/smeserver-manager/src/branch/master">master</a> at <a href="https://src.koozali.org/smeserver/smeserver-manager">smeserver/smeserver-manager</a></title>
|
|
<updated>2025-09-25T16:42:39+02:00</updated>
|
|
<id>80701: https://src.koozali.org/smeserver/smeserver-manager/commit/9c9ab9186966b5ba893528a79531bc608c42ac7b</id>
|
|
<content type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/9c9ab9186966b5ba893528a79531bc608c42ac7b" rel="nofollow">9c9ab9186966b5ba893528a79531bc608c42ac7b</a>
* Thu Sep 25 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="" rel="nofollow">brianr@koozali.org</a>&gt; 11.0.0-119.sme</content>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-manager/commit/9c9ab9186966b5ba893528a79531bc608c42ac7b" rel="alternate"></link>
|
|
<summary type="html"><a href="https://src.koozali.org/smeserver/smeserver-manager/commit/9c9ab9186966b5ba893528a79531bc608c42ac7b">9c9ab9186966b5ba893528a79531bc608c42ac7b</a>
* Thu Sep 25 2025 Brian Read &lt;<a href="mailto:brianr@koozali.org" data-markdown-generated-content="">brianr@koozali.org</a>&gt; 11.0.0-119.sme</summary>
|
|
<author>
|
|
<name>brianr</name>
|
|
<email>brianr@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
<entry>
|
|
<title>jpp pushed tag <a href="https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-11_el8_sme">11_0_0-11_el8_sme</a> to <a href="https://src.koozali.org/smeserver/smeserver-proftpd">smeserver/smeserver-proftpd</a></title>
|
|
<updated>2025-09-25T16:32:37+02:00</updated>
|
|
<id>80694: https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-11_el8_sme</id>
|
|
<link href="https://src.koozali.org/smeserver/smeserver-proftpd/src/tag/11_0_0-11_el8_sme" rel="alternate"></link>
|
|
<author>
|
|
<name>jpp</name>
|
|
<email>jpp@noreply.koozali.org</email>
|
|
</author>
|
|
</entry>
|
|
</feed>"""
|
|
|
|
def selftest_gitea_sample(org_name="smeserver", log_level="DEBUG"):
|
|
"""
|
|
Parse the embedded sample Atom and print the Rocket.Chat attachment JSON
|
|
that would be sent, one per entry. Does NOT call the webhook.
|
|
"""
|
|
logger = logging.getLogger("gitea_selftest")
|
|
logger.setLevel(getattr(logging, log_level.upper(), logging.DEBUG))
|
|
|
|
base_host = "https://src.koozali.org"
|
|
feed = feedparser.parse(SMESERVER_SAMPLE_ATOM)
|
|
|
|
out = []
|
|
for entry in feed.entries:
|
|
title_html = getattr(entry, "title", "")
|
|
summary_html = getattr(entry, "summary", "")
|
|
content_html = ""
|
|
if getattr(entry, "content", None):
|
|
content_html = entry.content[0].get("value", "")
|
|
updated = getattr(entry, "updated", "")
|
|
author_raw = getattr(entry, "author", "")
|
|
entry_link = getattr(entry, "link", "")
|
|
|
|
att = _gitea_build_attachment(
|
|
org_name=org_name,
|
|
title_html=title_html,
|
|
summary_html=summary_html,
|
|
content_html=content_html,
|
|
entry_link=entry_link,
|
|
updated=updated,
|
|
author_raw=author_raw,
|
|
base_host=base_host,
|
|
)
|
|
out.append(att)
|
|
|
|
# Pretty print to console & log
|
|
for i, att in enumerate(out, 1):
|
|
logger.debug("Attachment %d: %s", i, json.dumps(att, ensure_ascii=False))
|
|
print(f"\n--- Attachment {i} ---")
|
|
print(json.dumps(att, ensure_ascii=False, indent=2))
|
|
|
|
|
|
# ---------------------------
|
|
# Main loop
|
|
# ---------------------------
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Unified Feed -> Rocket.Chat notifier")
|
|
parser.add_argument("--sleep", type=int, default=1, help="Minutes to sleep between polls")
|
|
parser.add_argument("--one-off", action="store_true", help="Run once then exit")
|
|
parser.add_argument("--empty-db", action="store_true", help="Clear DB before start")
|
|
parser.add_argument("--feeds", type=str, default="", help="Comma-separated subset of feeds")
|
|
parser.add_argument("--log-level", type=str, default="INFO", help="Logging level")
|
|
parser.add_argument("--selftest-gitea", action="store_true",
|
|
help="Run built-in Gitea parser selftest using the embedded sample Atom (no network, no send)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
log_path = init_logging(args.log_level)
|
|
logging.info("FeedToRocket starting (log: %s)", log_path)
|
|
|
|
logging.debug("Loaded feeds: %s", FEED_CONFIG)
|
|
|
|
if args.selftest_gitea:
|
|
logging.info("Running Gitea selftest against embedded sample Atom…")
|
|
selftest_gitea_sample(org_name="smeserver", log_level=args.log_level)
|
|
return
|
|
|
|
|
|
setup_database()
|
|
if args.empty_db:
|
|
clear_database()
|
|
|
|
selected = {f.strip() for f in args.feeds.split(",") if f.strip()} if args.feeds else set()
|
|
domain_cache = {}
|
|
|
|
for name, conf in FEED_CONFIG.items():
|
|
if not conf.get("enabled"): continue
|
|
if selected and name not in selected: continue
|
|
send_startup_message(name, conf.get("chat_url"))
|
|
|
|
processors = {"bugzilla": process_bugzilla, "koji": process_koji, "wiki": process_wiki, "gitea": process_gitea}
|
|
sleep_sec = max(1, args.sleep) * 60
|
|
|
|
while True:
|
|
start = time.time()
|
|
for name, conf in FEED_CONFIG.items():
|
|
if not conf.get("enabled"): continue
|
|
if selected and name not in selected: continue
|
|
try:
|
|
proc = processors[conf["type"]]
|
|
proc(conf, args.one_off, domain_cache)
|
|
except Exception as e:
|
|
logging.exception("Feed %s failed: %s", name, e)
|
|
if args.one_off:
|
|
logging.info("One-off mode complete; exiting.")
|
|
break
|
|
elapsed = time.time() - start
|
|
time.sleep(max(1, sleep_sec - elapsed))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|