#!/usr/bin/env python3 """Empfänger für Grotti-Meldungen → Issue auf Forgejo/Gitea. Läuft getrennt von den Installationen. Der Token bleibt hier. export FORGEJO_URL=https://git.gundel.dev export FORGEJO_TOKEN=... export FORGEJO_REPO=dg/grotti python scripts/meldung_relay.py Port über RELAY_PORT (Standard 8780). Vor Forgejo mit Reverse-Proxy unter /hooks/grotti-meldung legen. """ from __future__ import annotations import json import os import urllib.error import urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer def env(name, default=""): return (os.environ.get(name) or default).strip() def create_issue(title, body): base = env("FORGEJO_URL", "https://git.gundel.dev").rstrip("/") repo = env("FORGEJO_REPO", "dg/grotti") token = env("FORGEJO_TOKEN") if not token: raise RuntimeError("FORGEJO_TOKEN fehlt.") url = "%s/api/v1/repos/%s/issues" % (base, repo) payload = json.dumps({"title": title[:120], "body": body}).encode("utf-8") request = urllib.request.Request( url, data=payload, headers={ "Content-Type": "application/json", "Authorization": "token %s" % token, }, method="POST", ) with urllib.request.urlopen(request, timeout=20) as response: data = json.loads(response.read().decode("utf-8")) return data.get("html_url") or data.get("url") or "" def issue_body(data): lines = [ data.get("text") or "", "", "---", "Version: %s" % (data.get("version") or "—"), "Seite: %s" % (data.get("page") or "—"), "Drucker: %s" % (data.get("printer_host") or "—"), ] err = (data.get("error_text") or "").strip() if err: lines.extend(["", "Fehlertext:", "```", err[:2000], "```"]) return "\n".join(lines) class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): return def do_GET(self): if self.path.split("?", 1)[0] in ("/", "/gesundheit", "/hooks/grotti-meldung"): self._json(200, {"ok": True}) return self._json(404, {"message": "Unbekannt."}) def do_POST(self): path = self.path.split("?", 1)[0] if path not in ("/", "/hooks/grotti-meldung"): self._json(404, {"message": "Unbekannt."}) return length = int(self.headers.get("Content-Length") or 0) raw = self.rfile.read(length) if length else b"{}" try: data = json.loads(raw.decode("utf-8")) except (UnicodeError, json.JSONDecodeError): self._json(400, {"message": "Kein JSON."}) return if not isinstance(data, dict) or not (data.get("text") or "").strip(): self._json(400, {"message": "text fehlt."}) return title = "Grotti: %s" % ((data.get("text") or "").strip().splitlines()[0][:80]) try: url = create_issue(title, issue_body(data)) except Exception as exc: self._json(502, {"message": str(exc)}) return self._json(201, {"ok": True, "url": url}) def _json(self, code, payload): body = json.dumps(payload).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def main(): port = int(env("RELAY_PORT", "8780") or 8780) httpd = ThreadingHTTPServer(("127.0.0.1", port), Handler) httpd.serve_forever() if __name__ == "__main__": main()