gitea-discord-proxy/app.py
disqualifier fbe66b1473 add proxy app to repo
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-22 19:48:41 -04:00

112 lines
3.6 KiB
Python

"""
discord webhook reformat proxy
drop-in for discord.com: swap the hostname on a gitea (discord-type) webhook
so it points here, e.g.
https://discord.com/api/webhooks/<id>/<token>
becomes
https://proxy.example.com/api/webhooks/<id>/<token>
the proxy receives gitea's native discord embed, rewrites only the embed
`description` into github's `[`hash`](url)` backtick style (hash backticked,
commit message collapsed to its first line, author kept), and forwards the
otherwise-untouched payload to the real discord webhook. everything gitea
already handled (title, branches, multiple commits, tags, colors) passes
through as-is.
run:
pip install fastapi httpx uvicorn
uvicorn discord_reformat_proxy:app --host 127.0.0.1 --port 8000
then set a gitea webhook (type: discord) to:
http://127.0.0.1:8000/api/webhooks/<id>/<token>
"""
import re
import logging
import httpx
from fastapi import FastAPI, Request, Response
log = logging.getLogger(__name__)
app = FastAPI()
DISCORD_BASE = "https://discord.com"
TIMEOUT = 10
# gitea commit line: [hash](url) <message possibly spanning lines> - author
# github commit line: [`hash`](url) <first line of message> - author
# match a hash-link, then the message up to an optional trailing " - author",
# stopping at the next commit line or end of string.
_COMMIT = re.compile(
r"\[`?([0-9a-f]{6,40})`?\]\((https?://[^)]+)\)"
r"\s+(.*?)"
r"(\s-\s[^\n]+?)?"
r"(?=\n\[|\Z)",
re.DOTALL,
)
def _commit_sub(m: "re.Match") -> str:
"""rebuild one commit line github-style: backtick hash, first message line only"""
short, url, message, author = m.group(1), m.group(2), m.group(3), m.group(4) or ""
first_line = message.strip().split("\n", 1)[0].strip()
return f"[`{short}`]({url}) {first_line}{author}"
def _reformat(description: str) -> str:
"""rewrite gitea commit lines into github-style (backtick hash, one line each)"""
return _COMMIT.sub(_commit_sub, description)
@app.get("/healthz")
async def healthz():
"""liveness probe"""
return {"ok": True}
@app.post("/api/webhooks/{webhook_id}/{token}")
async def proxy(webhook_id: str, token: str, request: Request):
"""reformat embed descriptions and forward to the real discord webhook"""
try:
payload = await request.json()
except Exception:
body = await request.body()
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.post(
f"{DISCORD_BASE}/api/webhooks/{webhook_id}/{token}",
content=body,
params=dict(request.query_params),
headers={"content-type": request.headers.get("content-type", "application/json")},
)
return _passthrough(resp)
if isinstance(payload, dict):
for embed in payload.get("embeds") or []:
if isinstance(embed, dict) and embed.get("description"):
embed["description"] = _reformat(embed["description"])
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.post(
f"{DISCORD_BASE}/api/webhooks/{webhook_id}/{token}",
json=payload,
params=dict(request.query_params),
)
return _passthrough(resp)
def _passthrough(resp: httpx.Response) -> Response:
"""return discord's response verbatim, preserving rate-limit headers"""
forward = {
k: v for k, v in resp.headers.items()
if k.lower().startswith("x-ratelimit") or k.lower() == "retry-after"
}
return Response(
content=resp.content,
status_code=resp.status_code,
media_type=resp.headers.get("content-type"),
headers=forward,
)