WebHooker/README.md

21 KiB
Raw Blame History

WebHooker

GitHub / Gitea webhook → Discord / Telegram dispatcher. Receives webhook events via Cloudflare Workers, applies filters, and routes formatted messages to Discord channels/threads and Telegram chats/topics. Forge-specific adapters live under server/lib/providers/ (GitHub + Gitea today; GitLab etc. can be added later).

Features

  • 28 event formatters — push, pull_request, issues, issue_comment, workflow_run, workflow_job, status, deployment, deployment_status, check_run, check_suite, ping, release, create, delete, star, fork, pull_request_review, pull_request_review_comment, commit_comment, member, label, milestone, discussion, discussion_comment, repository, code_scanning_alert, dependabot_alert (+ generic fallback, + custom webhooks)
  • Multi-provider webhooks — GitHub (X-Hub-Signature-256) and Gitea (X-Gitea-Signature) share one /webhook endpoint; the provider is auto-detected from headers
  • Per-group webhook ingress — every group can get its own POST /webhook/{groupId} URL + secret (Gitea, classic GitHub webhooks, and arbitrary custom JSON posts signed with X-WebHooker-Signature)
  • GitHub App tenant isolation — bind a group to a GitHub App installation id so only that org/user's events enter it
  • HMAC-SHA256 signature verification (Web Crypto API)
  • Filter by event type, repo, actor, action, branch, keyword (supports */? globs and /regex/)
  • Rich messages with color coding, author avatars, fields, and timestamps — rendered as Discord embeds and Telegram HTML
  • Route to Discord channels/threads and Telegram chats/topics (multi-target routes)
  • workflow_run / check_run progress is edited in place (single message updated as the run advances) on both platforms
  • Per-group webhook log channel — point a group at a Discord channel/thread or Telegram chat/topic and every webhook the group's routes dispatch is summarized there (/ per route × target)
  • GitHub OAuth for user actions (comment, edit comment, delete comment, merge, close, react)
  • Web UI config console (/admin) — manage routes and groups with GitHub OAuth + admin whitelist, view send logs
  • Discord Interactions Endpoint (Ed25519-verified) for /gh slash commands, message context-menu commands, PR merge/close buttons, and comment modals
  • Telegram /gh commands (login/logout/comment/merge/close) via the Telegram webhook, with avatar link-preview cards
  • Cloudflare KV for token/state/config/session storage + D1 for send logs and platform account links
  • Graceful degradation (webhook-only mode if Discord unavailable)

Architecture

GitHub Webhook → Cloudflare Worker (Nuxt 4 / Nitro)
                 ├── POST /webhook → verify → dedup → filter → format → Discord (REST) / Telegram (Bot API)
                 ├── POST /discord/interactions → verify (Ed25519) → handle command/button/modal
                 ├── POST /telegram/webhook → verify (secret token) → handle /gh commands
                 ├── GET  /auth/github → OAuth flow
                 ├── GET  /api/richheader → Telegram avatar link-preview card
                 ├── POST /api/* → user actions (Bearer token auth)
                 ├── /admin → routes, groups & send logs Web UI
                 └── GET  /health → status check
  • Cloudflare Worker — HTTP ingress, signature verification, routing, platform dispatch
  • Interactions Endpoint — HTTPS callback (no Discord Gateway connection, no Durable Object); the bot stays offline and commands are registered via the API
  • KV — token storage (token:{userId}), OAuth state (state:{hex}), route config (config:routes), group config (config:groups), admin sessions (session:{id}), delivery dedup (delivery:{id}), message-update tracking (msg:*)
  • D1 — send logs (send_logs), Discord↔GitHub links (discord_links), Telegram↔GitHub links (telegram_links)

Quick Start

bun install          # package manager is bun (lockfile: bun.lock)
cp .env.example .dev.vars   # Fill in secrets for local dev
bun run build        # Production build first (wrangler dev serves the built worker)
bunx wrangler dev    # Start local dev server

Configuration

Secrets (.dev.vars for local, Worker Secrets for production)

Variable Description
GITHUB_WEBHOOK_SECRET Webhook secret from GitHub
GITEA_WEBHOOK_SECRET Webhook secret from Gitea (required only to receive Gitea webhooks)
GITHUB_APP_ID GitHub App ID (used by the App install flow to resolve the installing account)
GITHUB_PRIVATE_KEY App private key (PKCS#8 PEM; used by the App install flow; optional)
GITHUB_CLIENT_ID OAuth client ID
GITHUB_CLIENT_SECRET OAuth client secret
DISCORD_TOKEN Bot token
DISCORD_PUBLIC_KEY Discord application public key (from the Developer Portal) — required for interactions
DISCORD_APPLICATION_ID Discord application id (optional; auto-resolved via GET /oauth2/applications/@me if omitted)
TELEGRAM_TOKEN Telegram bot token (from BotFather) — required for Telegram routes
TELEGRAM_WEBHOOK_SECRET Optional secret token for POST /telegram/webhook verification
TELEGRAM_RICH_HEADER_HOST Optional base URL overriding the built-in GET /api/richheader for Telegram avatar cards
BASE_URL Public URL for OAuth callbacks and the Telegram webhook sync
ADMIN_USER_IDS Comma-separated GitHub user IDs (or logins) allowed to access /admin
ALLOW_SELF_SIGNUP 1 to give access-less GitHub users a personal group on first login (default off)
AUDIT_RETENTION_DAYS Audit-log retention in days for the scheduled cleanup (default 90)
NUXT_PUBLIC_DOCS_URL Optional docs site URL used by the landing page
NUXT_PUBLIC_REPO_URL Optional GitHub repo URL used by the landing page
NUXT_PUBLIC_LEGAL_CONTACT Optional contact shown on /terms and /privacy

Routes

Routes are stored in KV (config:routes as JSON). There are no default routes — every route (including its target) must be defined explicitly, either via the Web UI (/admin) or by storing a JSON array in KV. A route may carry multiple targets, so one rule can forward to several channels at once:

[
  {
    "id": "all-push",
    "name": "Push Events",
    "enabled": true,
    "groupId": "default",
    "filters": [{ "type": "event", "match": "push" }],
    "stop": true,
    "targets": [
      { "platform": "discord", "channelId": "CHANNEL_ID" },
      { "platform": "telegram", "chatId": "-1001234567890" }
    ]
  }
]

target.platform selects the push target: discord (default) or telegram. Discord targets require target.channelId (optional threadId for a thread); Telegram targets require target.chatId (optional topicId for a topic). The legacy singular target field is still migrated automatically. There is no fallback to a default channel.

Set discordRoleIds on a route to ping Discord roles (身份组) whenever it fires:

{
  "id": "release-notify",
  "name": "Notify on Release",
  "enabled": true,
  "groupId": "default",
  "discordRoleIds": ["111111111111111111"],
  "filters": [{ "type": "event", "match": "release" }],
  "targets": [{ "platform": "discord", "channelId": "CHANNEL_ID" }]
}

Routes belong to groups (KV config:groups) that scope admin access and can restrict which org/user events flow in — including which source platform (providers: github / gitea). See config.example.yaml and docs/guide/configuration.md for the full schema.

Web UI (/admin)

The built-in config console lets you manage routes and groups in the browser (add / edit / delete / toggle / reorder), inspect send logs, manage group members and invite links, and read the audit log — no KV access needed:

  1. Set ADMIN_USER_IDS to the GitHub user IDs (or logins) allowed to manage the console, e.g. ADMIN_USER_IDS=12345,RhenCloud.
  2. Visit /admin and sign in with GitHub. Users with no access get 403 — unless ALLOW_SELF_SIGNUP=1 (they receive a personal group) or they follow a group invite link.
  3. Changes are written to KV immediately and picked up by the webhook pipeline.

Sign out at /admin/logout.

Access model. Every group has members with a role: owner (manage group, members, invites; edit routes), admin (edit routes, view logs), or viewer (read-only). Super admins (ADMIN_USER_IDS) bypass everything. Legacy adminIds are read as owners. Owners generate single-use, 7-day invite links from the group page; group admins and viewers can browse, admins edit, owners administer. All admin operations (logins, group/route/member/invite changes) are recorded in the D1 audit_logs table, pruned after AUDIT_RETENTION_DAYS (default 90).

See config.example.yaml for full syntax examples.

Filter Types

Type Matches Notes
event push, pull_request, issues, etc. GitHub event name
repo org/repo full name
actor Sender login
action opened, closed, published, etc.
branch Branch name Works for push, PR/review, create/delete, workflow_run, workflow_job, check_suite, deployment, code_scanning_alert
keyword Text in payload body All filters support */? globs and /regex/ patterns (case-insensitive)

Set exclude: true to invert any filter. See the Filter Tutorial for the pattern syntax.

API

Health

  • GET /health — Returns {"status": "ok"}

OAuth

  • GET /auth/github — Start GitHub OAuth flow (redirects to GitHub)
  • GET /auth/github/callback — OAuth callback (exchanges code for token; admin session / Discord link / Telegram link)
  • DELETE /auth/token/:userId — Revoke user token

Actions (require Authorization: Bearer <token> header)

  • POST /api/comment — Create issue comment
  • POST /api/merge — Merge pull request
  • POST /api/close — Close pull request
  • POST /api/react — Add reaction to issue

Admin (require admin OAuth session)

  • GET /admin — Config console UI
  • GET /admin/login — Start admin sign-in (GitHub OAuth)
  • GET /admin/logout — Sign out
  • GET /admin/invite?token=… — Accept a group invite (browser page)
  • GET /admin/api/routes — List routes
  • PUT /admin/api/routes — Replace routes (owner/admin per group)
  • GET /admin/api/groups — List groups + your role in each
  • PUT /admin/api/groups — Replace groups (super: all; owner: own groups only)
  • GET /admin/api/groups/:groupId/routes — List a group's routes
  • PUT /admin/api/groups/:groupId/routes — Replace a group's routes
  • POST /admin/api/groups/:groupId/invites — Create invite link (owner)
  • GET /admin/api/groups/:groupId/invites — List pending invites (owner)
  • DELETE /admin/api/invites/:token — Revoke an invite (owner)
  • GET /admin/api/audit — Audit log (scoped)
  • GET /admin/api/me — Current session / scope / roles
  • GET /admin/api/logs — Send logs (scoped)
  • GET /admin/api/logs/:id — Single send-log entry

GitHub App Setup

1. Create App

  1. Go to https://github.com/settings/apps/new
  2. Fill in:
    • GitHub App name: WebHooker (or your choice)
    • Homepage URL: your domain
    • Webhook URL: https://your-domain/webhook
    • Webhook secret: generate and copy to GITHUB_WEBHOOK_SECRET
  3. Set permissions:
    • Repository permissions: Contents (read), Issues (write), Pull requests (write), Metadata (read), Checks (read), Deployments (read), Discussions (read), Code scanning alerts (read), Dependabot alerts (read)
    • Organization permissions: Members (read) — if needed
  4. Subscribe to events:
    • Push, Pull request, Issues, Issue comment, Workflow run, Workflow job, Status, Deployment, Deployment status, Ping, Release, Create, Delete, Star, Fork, Check run, Check suite, Pull request review, Pull request review comment, Commit comment, Member, Label, Milestone, Discussion, Discussion comment, Repository, Code scanning alert, Dependabot alert
  5. Generate private key — optional; GITHUB_APP_ID + GITHUB_PRIVATE_KEY are only used by the App install flow to resolve the installing account's login on the post-install page.

2. Install App

  1. After creation, go to the App settings page
  2. Click "Install App" → select org/user
  3. Choose repositories to monitor

3. Configure OAuth

  1. Go to App → OAuth settings
  2. Set Callback URL: https://your-domain/auth/github/callback
  3. Copy Client ID and Client Secret to env

Discord Bot Setup

Create a bot at https://discord.com/developers/applications, copy its token to DISCORD_TOKEN.

OAuth2 Invite

Add the bot to your server with the bot scope and the following permissions:

Permission Value Why
View Channels 1024 See the target channel to post messages
Send Messages 2048 Send embeds/messages to channels
Send Messages in Threads 274877906944 Send to threads when a route targets threadId

Combined permission integer: 274877910016

Invite URL (replace CLIENT_ID with your bot's client ID). The applications.commands scope is required so the slash / context-menu commands can be registered:

https://discord.com/oauth2/authorize?client_id=YOUR_BOT_CLIENT_ID&permissions=274877910016&scope=bot+applications.commands

Interactions Endpoint

Copy the application Public Key (Developer Portal → General Information) to DISCORD_PUBLIC_KEY and set the Interactions Endpoint URL to https://your-domain/discord/interactions. All interactions (slash commands, buttons, modals) are verified with Ed25519 signatures.

The bot never connects to the Discord Gateway, so it shows as offline — messaging is unaffected (always REST).

Bot Commands (comment on GitHub as yourself)

The bot registers native slash and message context-menu commands, synced by the scheduled trigger (every 5 minutes): per-guild for instant availability, and globally (24h dedup, ~1h propagation). After /gh login you can comment on issues/PRs as yourself, edit/delete your comments, and merge/close PRs via buttons — all replies are ephemeral and GitHub enforces permission.

/gh login  /gh logout
/gh comment add|edit|del  link:<url>      (or right-click a notification → Apps → GitHub: 添加/编辑/删除评论)

See the full reference in the Bot Commands guide.

Telegram Bot Setup

  1. Create a bot with @BotFather and copy its token to TELEGRAM_TOKEN.
  2. (Optional) Set TELEGRAM_WEBHOOK_SECRET; the webhook registration passes it to Telegram as the secret_token, and POST /telegram/webhook verifies it with a timing-safe compare.
  3. The worker syncs the webhook from the scheduled trigger (setWebhook to {BASE_URL}/telegram/webhook), so no manual setWebhook call is needed — just make sure BASE_URL is set.
  4. Add the bot to a group (or enable topics) and route events to chatId / topicId in the route config.

In Telegram, /gh commands work by replying to a notification message: /gh login, /gh logout, /gh comment <text>, /gh merge, /gh close. See the Bot Commands guide.

Avatars are rendered as a link-preview card using the built-in GET /api/richheader (overridable with TELEGRAM_RICH_HEADER_HOST).

Deployment

# Set secrets in Cloudflare
bunx wrangler secret put GITHUB_WEBHOOK_SECRET
bunx wrangler secret put GITHUB_CLIENT_ID
bunx wrangler secret put GITHUB_CLIENT_SECRET
bunx wrangler secret put DISCORD_TOKEN
bunx wrangler secret put DISCORD_PUBLIC_KEY
bunx wrangler secret put TELEGRAM_TOKEN
bunx wrangler secret put ADMIN_USER_IDS

# Create KV namespace
bunx wrangler kv namespace create KV
# Update wrangler.jsonc with the KV namespace ID

# Create D1 database and run migrations
bunx wrangler d1 create webhooker
# Update wrangler.jsonc d1_databases with the database ID
bun run db:migrate:prod   # apply migrations to the remote D1 database

# Deploy
bunx wrangler deploy

Development

bun run dev           # Nuxt dev server (HMR)
bun run typecheck     # Type checking
bun run lint          # ESLint
bun test              # Unit tests

Supported Events

Event Formatter
push Commit list, branch, author
pull_request PR title, branch, diff stats
issues Issue title, labels, assignees
issue_comment Comment body, issue reference
workflow_run Workflow status, conclusion, duration (edited in place)
workflow_job Job name, status, conclusion
status Commit status, context, state
deployment Environment, ref, task
deployment_status Environment, status, commit ref
check_run Status, conclusion, details URL (edited in place)
check_suite Suite conclusion, head branch, commit
ping Webhook confirmation
release Tag, body, assets
create / delete Branch/tag creation/deletion
star Star count, repository
fork Fork source → target
pull_request_review Review state, body preview
pull_request_review_comment Inline code comment, file path, line
commit_comment Commit SHA, comment body
member Collaborator add/remove
label Label name, color, description
milestone Progress bar, open/closed counts, due date
discussion Discussion title, category, action
discussion_comment Comment body, discussion reference
repository Repo rename/transfer details
code_scanning_alert Severity, rule ID, file path
dependabot_alert Severity, package, vulnerable range, fix version

Any other event type falls back to the generic formatter (event type, action, actor, repo, raw payload).

License

MIT