#!/usr/bin/env python3 """Gitea to ntfy Webhook Bridge - Translates Gitea events to ntfy notifications""" import os import sys import json import urllib.request from http.server import HTTPServer, BaseHTTPRequestHandler # Force unbuffered output sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1) sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1) NTFY_URL = os.environ.get("NTFY_URL", "https://ntfy.vish.gg") NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "homelab-alerts") class WebhookHandler(BaseHTTPRequestHandler): def do_GET(self): """Health check endpoint""" self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write(b"Gitea-ntfy bridge OK\n") print(f"Health check from {self.client_address[0]}", flush=True) def do_POST(self): content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length) try: data = json.loads(body) if body else {} event_type = self.headers.get("X-Gitea-Event", "unknown") print(f"Received {event_type} event from {self.client_address[0]}", flush=True) title, message, tags, priority = self.format_message(event_type, data) if title and message: print(f"Sending notification: {title}", flush=True) self.send_ntfy(title, message, tags, priority) self.send_response(200) else: print(f"Ignoring event type: {event_type}", flush=True) self.send_response(204) # No content to send except Exception as e: print(f"Error processing webhook: {e}", flush=True) self.send_response(500) self.end_headers() def format_message(self, event_type, data): """Format Gitea event into ntfy message""" repo = data.get("repository", {}).get("full_name", "unknown") sender = data.get("sender", {}).get("login", "unknown") title = None message = None tags = "git" priority = "default" if event_type == "push": commits = data.get("commits", []) branch = data.get("ref", "").replace("refs/heads/", "") count = len(commits) title = f"Push to {repo}" message = f"{sender} pushed {count} commit(s) to {branch}" if commits: message += f"\n\n* {commits[0].get('message', '').split(chr(10))[0]}" if count > 1: message += f"\n* ... and {count - 1} more" tags = "package" elif event_type == "pull_request": action = data.get("action", "") pr = data.get("pull_request", {}) pr_title = pr.get("title", "") pr_num = pr.get("number", "") title = f"PR #{pr_num} {action}" message = f"{repo}: {pr_title}\nBy: {sender}" tags = "twisted_rightwards_arrows" if action == "opened": priority = "high" elif event_type == "issues": action = data.get("action", "") issue = data.get("issue", {}) issue_title = issue.get("title", "") issue_num = issue.get("number", "") title = f"Issue #{issue_num} {action}" message = f"{repo}: {issue_title}\nBy: {sender}" tags = "clipboard" elif event_type == "release": action = data.get("action", "") release = data.get("release", {}) tag = release.get("tag_name", "") title = f"Release {tag}" message = f"{repo}: New release {action}\n{release.get('name', tag)}" tags = "rocket" priority = "high" elif event_type == "create": ref_type = data.get("ref_type", "") ref = data.get("ref", "") title = f"New {ref_type}: {ref}" message = f"{repo}\nCreated by: {sender}" tags = "sparkles" elif event_type == "delete": ref_type = data.get("ref_type", "") ref = data.get("ref", "") title = f"Deleted {ref_type}: {ref}" message = f"{repo}\nDeleted by: {sender}" tags = "wastebasket" return title, message, tags, priority def send_ntfy(self, title, message, tags="git", priority="default"): """Send notification to ntfy""" url = f"{NTFY_URL}/{NTFY_TOPIC}" headers = { "Title": title, "Tags": tags, "Priority": priority, } req = urllib.request.Request(url, data=message.encode('utf-8'), headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=10) as resp: print(f"Sent: {title} -> {resp.status}") except Exception as e: print(f"Failed to send ntfy: {e}") def log_message(self, format, *args): print(f"[{self.log_date_time_string()}] {format % args}") if __name__ == "__main__": server = HTTPServer(("0.0.0.0", 8095), WebhookHandler) print(f"Gitea-ntfy bridge running on :8095 -> {NTFY_URL}/{NTFY_TOPIC}") server.serve_forever()