# Dis2Hook — Webhook Relay to Discord Bot Dis2Hook receives webhooks from anything that can send an HTTP POST — Gitea, Gogs, Proxmox notifications, Uptime Kuma, home-automation scripts — formats them as Markdown, and relays them into Discord channels **through the Discord Bot API** (not Discord's incoming webhooks, so one bot identity serves every channel). It is built to live in a small **Alpine Linux LXC on Proxmox** and is configured entirely from a built-in web UI. ``` source A ──▶ POST /hook/gitea-ci ──┐ source B ──▶ POST /hook/kuma ──────┼──▶ Dis2Hook ──▶ Discord Bot API ──▶ #channels source C ──▶ POST /hook/backup ────┘ ``` ## Features - **Web UI configurator** — patch in any number of webhook sources, each routed to its own Discord server/channel, from a single console page. - **Markdown templates** — shape each source's message with Discord Markdown and `{dotted.path[0]}` placeholders resolved from the incoming JSON payload; leave the template empty to relay the raw payload as a pretty-printed JSON block. - **Event filters** — per-source rules (`equals` / `contains` / `exists` on any payload path) so only the events you care about reach Discord. - **Drag-and-drop template builder** — Dis2Hook remembers the last event each source received; the UI shows its JSON keys as draggable chips you drop straight into the template (or click to insert), so templates are built from real data instead of guesswork. - **Image embedding** — point a source at any payload path holding an image: an internet URL (embedded by Discord), a **LAN URL** like `http://192.168.x.x/…` (Dis2Hook fetches it and uploads it as an attachment, since Discord can't reach your LAN), or **base64 image data / data-URIs** (decoded and attached). JPEG/PNG/GIF/WebP, up to 8 MB. - **Heartbeat** — optional periodic pulse to a channel with uptime and relay counts. - **Status updates** — optional online/offline announcements when the service starts or stops. - **Secrets stay local** — bot token and admin key live in a `token.json` generated at install time; it is never pulled from, nor pushed to, this repo. - **One-file updater** — pulls fresh `app.py` / `index.html` straight from the repo, validates them, backs up the old ones, and restarts the service. ## Install > **Run this inside an existing Alpine LXC — never on the Proxmox host.** > The installer refuses to run if it detects Proxmox VE. 1. On the Proxmox host, enter your Alpine container: `pct enter ` 2. Run the one-liner: ```sh wget -qO- https://gogs.av2x.dev/av2x/Dis2Hook/raw/master/install.sh | ash ``` The installer will: - verify it is running inside Alpine (and not on a Proxmox host), - install `python3`, `py3-flask`, `py3-requests` via `apk`, - pull `app.py`, `index.html`, `update.sh`, and the OpenRC service script **directly from this repo**, - prompt for your **Discord bot token** (Developer Portal → your app → Bot), - generate `/opt/dis2hook/token.json` (mode `600`) with the token and a fresh random **admin key**, printed once at the end — save it, - register and start the `dis2hook` OpenRC service. Non-interactive install: ```sh DISCORD_BOT_TOKEN=xxxxx wget -qO- https://gogs.av2x.dev/av2x/Dis2Hook/raw/master/install.sh | ash ``` Optional environment overrides: `D2H_BRANCH` (defaults to `master`, auto-falls back to `main`), `D2H_DIR` (default `/opt/dis2hook`), `D2H_PORT` (default `8823`). ## First-time setup 1. Create a bot at the Discord Developer Portal and copy its token. 2. Invite it to your server with the **Send Messages** and **View Channel** permissions (OAuth2 URL generator → scope `bot`). 3. In Discord, enable Developer Mode, right-click a channel → **Copy Channel ID**. 4. Open `http://:8823/`, unlock with the admin key, add a source, paste the channel ID, save, and hit **Send test message**. 5. Point your service at the source's webhook URL, sending the source secret in the `X-Hook-Secret` header (or `?secret=` query, or a `Bearer` token). ### Sending a webhook by hand ```sh curl -X POST "http://:8823/hook/gitea-ci" \ -H "Content-Type: application/json" \ -H "X-Hook-Secret: " \ -d '{"action":"opened","repository":{"full_name":"av2x/Dis2Hook"}}' ``` ### Template example ``` 🔔 **{repository.full_name}** — {pusher.name} pushed to `{ref}` > {commits[0].message} {commits[0].url} ``` Unresolved placeholders render as empty strings; objects/lists render as compact JSON. Messages are truncated to Discord's 2000-character limit. ### Building templates from a real event Once a source has received at least one webhook, open its strip in the UI: under the template you'll find **"Payload keys from the last received event"** — every key/value pair from that event as a chip. Drag chips into the template (or click them) to insert `{path}` placeholders at the cursor; hover a chip to preview its value. Hit **↻ refresh** after new events arrive. Blue chips are values that look like images. ### Embedding images Set a source's **Image — payload path** to the field holding the picture (drag a blue chip into it), and pick a delivery mode: - **fetch & upload** (default) — Dis2Hook downloads the URL itself, or decodes base64 / `data:` image data, and uploads it to Discord as an attachment. Use this for LAN URLs (e.g. `http://192.168.51.234:8000/...`) and base64 fields — Discord's servers can never reach your LAN directly. - **embed link** — Discord fetches the URL itself; only works for URLs reachable from the public internet. Example for a Bambu Lab print notifier: template `🖨️ **{printer}** — {title}\n\`{filename}\` after {duration}` with image path `image` (base64 snapshot) or `finish_photo_url` (LAN URL) in fetch & upload mode. ## Updating Pull the latest `app.py` and `index.html` directly from the repo and restart: ```sh /opt/dis2hook/update.sh # app.py + index.html /opt/dis2hook/update.sh --all # also refresh update.sh and the service script ``` Downloads are validated (version marker + compile check) before they replace anything, previous files are kept as `*.bak`, and **`token.json` and `config.json` are never touched**. ## Files | Path | Purpose | In repo? | |---|---|---| | `/opt/dis2hook/app.py` | relay service (Flask) | yes | | `/opt/dis2hook/index.html` | web UI | yes | | `/opt/dis2hook/update.sh` | updater | yes | | `/etc/init.d/dis2hook` | OpenRC service (`dis2hook.initd`) | yes | | `/opt/dis2hook/token.json` | **bot token + admin key — generated at install, mode 600** | **never** | | `/opt/dis2hook/config.json` | sources & settings, managed by the web UI | never | | `/var/log/dis2hook.log` | service log | — | ## Service management ```sh rc-service dis2hook status|start|stop|restart tail -f /var/log/dis2hook.log ``` ## Security notes - `token.json` is generated locally, `chmod 600`, and listed in `.gitignore`; treat the admin key like a password — it is the only credential for the UI/API. - Every source has its own shared secret, compared in constant time; requests with a wrong or missing secret are rejected with `401`. - Dis2Hook serves plain HTTP. Keep it on a trusted LAN/VLAN, or front it with a reverse proxy (Caddy, nginx, Nginx Proxy Manager) for TLS if sources send webhooks across the internet. - Relayed messages are sent with all mentions disabled, so a hostile payload cannot ping `@everyone`. - The UI cannot change the listen host/port — edit `config.json` and restart if you need to, so a bad save can never lock you out. ## How it compares to Discohook Discohook is a superb editor for Discord's *incoming webhooks* — you compose a message and Discord hosts the endpoint. Dis2Hook is the opposite direction: **it hosts the endpoint**, accepts webhooks from your own services, filters and reformats them, and delivers via a bot token — so one self-hosted relay fans out many sources to many servers and channels, with heartbeat and uptime status built in. ## Uninstall ```sh rc-service dis2hook stop; rc-update del dis2hook default rm -rf /opt/dis2hook /etc/init.d/dis2hook /var/log/dis2hook.log ```