feat: add Telegram push support with multi-target routes

Add a platform-aware route target system so a single route can forward
to several destinations at once (e.g. a Discord channel and a Telegram
group). Route.target becomes Route.targets[] with per-entry platform,
channelId/threadId for Discord and chatId/topicId for Telegram; the
legacy single-target format is normalized on load and accepted by the
admin API.

Implement the Telegram driver with HTML rendering and Bot API
sendMessage (chat_id + message_thread_id for topics, retry on 429/5xx),
plus /gh commands served over POST /telegram/webhook: login, logout,
comment, merge and close. The comment/merge/close commands resolve the
issue or PR from the replied-to notification message. OAuth binding now
stores a D1 telegram_links mapping and replies with a confirmation.

Sync the Telegram webhook from the scheduled trigger via setWebhook.
This commit is contained in:
RhenCloud 2026-08-03 05:17:59 +08:00
parent dcbe93be91
commit bd7a8f2632
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
39 changed files with 1165 additions and 162 deletions

View file

@ -15,6 +15,7 @@ https://your-worker.workers.dev
| `GET` | `/health` | None | Health check |
| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion |
| `POST` | `/discord/interactions` | Ed25519 signature | Discord interactions (slash commands, buttons, modals) |
| `POST` | `/telegram/webhook` | Secret token | Telegram updates (bot `/gh` commands) |
| `GET` | `/auth/github` | None | Start GitHub OAuth flow |
| `GET` | `/auth/github/callback` | None | OAuth callback |
| `DELETE` | `/auth/token/:userId` | None | Revoke user token |
@ -38,7 +39,7 @@ See [Configuration → Web UI](../guide/configuration.md#web-ui) for setup. Admi
- `GET /admin` — Serves the config console HTML
- `GET /admin/api/routes` — Returns `{ "routes": Route[] }`
- `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id, name, enabled, `groupId`, filters — empty only allowed for `fallback` routes — and string `target.channelId`) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }`.
- `PUT /admin/api/routes` — Body `{ "routes": Route[] }`; validates each route (id pattern, unique id, name, enabled, `groupId`, filters — empty only allowed for `fallback` routes — and platform-aware target: `target.channelId` for Discord, `target.chatId` for Telegram) and persists to KV `config:routes`. Returns `200 { ok, count }` or `400 { error }` / `401 { error }`.
## Health Check

View file

@ -31,13 +31,15 @@ src/
│ └── *.ts # push, pull-request, issues, comments, workflow, release, repo, ...
├── drivers/ # Platform drivers (pluggable push targets)
│ ├── types.ts # PlatformDriver interface + SendResult
│ ├── index.ts # getDriver() registry (discord + telegram stub)
│ ├── index.ts # getDriver() registry (discord + telegram)
│ ├── discord/ # index.ts (driver), render.ts (NeutralMessage → embed),
│ │ # rest.ts, interactions.ts, commands.ts
│ └── telegram/ # TelegramDriver stub (not implemented yet)
│ └── telegram/ # index.ts (driver), render.ts (NeutralMessage → Telegram HTML),
│ # rest.ts (chat_id + message_thread_id), updates.ts (webhook verify),
│ # commands.ts (/gh login|logout|comment|merge|close + reply parsing)
├── github/ # GitHub OAuth + as-user actions
│ ├── oauth.ts # OAuth URL, callback token exchange, getUserOctokit, actions
│ └── store.ts # KV token CRUD + D1 discord-link mapping
│ └── store.ts # KV token CRUD + D1 discord-link/telegram-link mapping
├── web/ # HTTP UI/API routes
│ ├── oauth-routes.ts # GET /auth/github, callback, DELETE /token/:userId (KV state)
│ ├── action-routes.ts # POST /api/comment|merge|close|react (Bearer token auth via KV lookup)

View file

@ -14,6 +14,7 @@ WebHooker requires several secrets to function. For local development, store the
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
| `DISCORD_TOKEN` | Discord bot token |
| `TELEGRAM_TOKEN` | Telegram bot token (from BotFather) — required for Telegram routes |
### Optional Secrets
@ -21,6 +22,7 @@ WebHooker requires several secrets to function. For local development, store the
| ------------------------ | ----------------------------------------------------------------------------- | --------------------------------- |
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
| `DISCORD_APPLICATION_ID` | Discord application id; auto-resolved when omitted | Auto-resolved |
| `TELEGRAM_WEBHOOK_SECRET`| Secret token for `POST /telegram/webhook` verification (X-Telegram-Bot-Api-Secret-Token) | Disabled (no verification) |
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
@ -54,7 +56,7 @@ The console lets you add, edit, delete, and toggle routes. Saved routes are writ
## Routes
Routes define which events get forwarded to which Discord channels. They are stored in Cloudflare KV under the key `config:routes` as a JSON array.
Routes define which events get forwarded to which channel (Discord or Telegram). They are stored in Cloudflare KV under the key `config:routes` as a JSON array.
There are **no default routes** — each route must define its own target. If no routes are configured, no events are forwarded.
@ -71,14 +73,17 @@ There are **no default routes** — each route must define its own target. If no
{ "type": "event", "match": "push" },
{ "type": "repo", "match": "org/repo", "exclude": false }
],
"target": {
"channelId": "REQUIRED_CHANNEL_ID",
"threadId": "OPTIONAL_THREAD_ID"
}
"targets": [
{
"platform": "discord",
"channelId": "REQUIRED_CHANNEL_ID",
"threadId": "OPTIONAL_THREAD_ID"
}
]
}
```
`target.channelId` is required and used as-is; there is no fallback to a default channel.
Each entry of `targets` is a push destination, so one route can forward to several channels at once (e.g. a Discord channel **and** a Telegram group). `target.platform` selects the platform: `discord` (default) or `telegram`. For **Discord**, `target.channelId` is required (a thread in `target.threadId` is optional). For **Telegram**, `target.chatId` (the group/supergroup chat id, e.g. `-1001234567890`) is required and `target.topicId` (the `message_thread_id` of a topic, equivalent of a Discord thread) is optional. There is no fallback to a default channel.
Other route fields:
@ -102,10 +107,13 @@ Other route fields:
{ "type": "event", "match": "pull_request" },
{ "type": "actor", "match": "[bot]", "exclude": true }
],
"target": {
"channelId": "1234567890",
"threadId": "9876543210"
}
"targets": [
{
"platform": "telegram",
"chatId": "-1001234567890",
"topicId": "9876543210"
}
]
}
]
```

View file

@ -31,6 +31,7 @@ npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put DISCORD_TOKEN
npx wrangler secret put DISCORD_PUBLIC_KEY # Discord app public key (Developer Portal) — required for interactions
npx wrangler secret put TELEGRAM_TOKEN # Telegram bot token (BotFather) — required for Telegram routes
npx wrangler secret put ADMIN_USER_IDS # comma-separated GitHub IDs/logins allowed into the Web UI
```

View file

@ -15,6 +15,7 @@ https://your-worker.workers.dev
| `GET` | `/health` | 无 | 健康检查 |
| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 |
| `POST` | `/discord/interactions` | Ed25519 签名 | Discord 交互斜杠命令、按钮、modal |
| `POST` | `/telegram/webhook` | Secret token | Telegram 更新bot `/gh` 命令) |
| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 |
| `GET` | `/auth/github/callback` | 无 | OAuth 回调 |
| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token |
@ -38,7 +39,7 @@ https://your-worker.workers.dev
- `GET /admin` — 提供配置控制台 HTML
- `GET /admin/api/routes` — 返回 `{ "routes": Route[] }`
- `PUT /admin/api/routes` — 请求体为 `{ "routes": Route[] }`校验每条路由id 格式、唯一 id、name、enabled、groupId、过滤器、字符串 `target.channelId`)并持久化到 KV `config:routes`。返回 `200 { ok, count }``400 { error }` / `401 { error }`
- `PUT /admin/api/routes` — 请求体为 `{ "routes": Route[] }`校验每条路由id 格式、唯一 id、name、enabled、groupId、过滤器、平台感知的 targetDiscord 需 `target.channelId`Telegram 需 `target.chatId`)并持久化到 KV `config:routes`。返回 `200 { ok, count }``400 { error }` / `401 { error }`
## 健康检查

View file

@ -31,13 +31,15 @@ src/
│ └── *.ts # push、pull-request、issues、comments、workflow、release、repo 等
├── drivers/ # 平台驱动(可插拔推送目标)
│ ├── types.ts # PlatformDriver 接口 + SendResult
│ ├── index.ts # getDriver() 注册表discord + telegram 占位
│ ├── index.ts # getDriver() 注册表discord + telegram
│ ├── discord/ # index.ts (驱动)、render.ts (NeutralMessage → embed)、
│ │ # rest.ts、interactions.ts、commands.ts
│ └── telegram/ # TelegramDriver 占位(未实现)
│ └── telegram/ # index.ts (驱动)、render.ts (NeutralMessage → Telegram HTML)、
│ # rest.ts (chat_id + message_thread_id)、updates.ts (webhook 验签)、
│ # commands.ts (/gh login|logout|comment|merge|close + 引用消息解析)
├── github/ # GitHub OAuth + 以用户身份操作
│ ├── oauth.ts # OAuth URL、回调 Token 交换、getUserOctokit、操作
│ └── store.ts # KV Token CRUD + D1 discord-link 映射
│ └── store.ts # KV Token CRUD + D1 discord-link/telegram-link 映射
├── web/ # HTTP UI/API 路由
│ ├── oauth-routes.ts # GET /auth/github、回调、DELETE /token/:userId (KV 状态)
│ ├── action-routes.ts # POST /api/comment|merge|close|react (通过 KV 查找进行 Bearer Token 鉴权)

View file

@ -14,6 +14,7 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
| `DISCORD_TOKEN` | Discord Bot Token |
| `TELEGRAM_TOKEN` | Telegram Bot TokenBotFather 获取)—— Telegram 路由必需 |
### 可选密钥
@ -23,6 +24,7 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID或登录名逗号分隔 | 未设置时 WebUI 关闭 |
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
| `DISCORD_APPLICATION_ID` | Discord 应用 ID省略时自动获取 | 自动获取 |
| `TELEGRAM_WEBHOOK_SECRET`| `POST /telegram/webhook` 验签密钥X-Telegram-Bot-Api-Secret-Token | 未设置时不校验 |
## Web 控制台
@ -54,7 +56,7 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
## 路由
路由定义了哪些事件被转发到哪些 Discord 频道。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 `config:routes`
路由定义了哪些事件被转发到哪些频道Discord 或 Telegram。它们以 JSON 数组形式存储在 Cloudflare KV 中,键为 `config:routes`
**没有默认路由**——每条路由必须自行定义目标频道。若未配置任何路由,则不会转发任何事件。
@ -71,14 +73,17 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
{ "type": "event", "match": "push" },
{ "type": "repo", "match": "org/repo", "exclude": false }
],
"target": {
"channelId": "必填频道ID",
"threadId": "可选线程ID"
}
"targets": [
{
"platform": "discord",
"channelId": "必填频道ID",
"threadId": "可选线程ID"
}
]
}
```
`target.channelId` 必填且按原样使用,不存在默认频道回退。
`targets` 数组的每一项是一个推送目标,因此一条路由可同时转发到多个频道(例如同时发到 Discord 频道 **和** Telegram 群组)。`target.platform` 选择推送目标:`discord`(默认)或 `telegram`。**Discord** 需 `target.channelId``target.threadId` 可选的子区);**Telegram** 需 `target.chatId`(群组/超级群组聊天 id`-1001234567890``target.topicId`(话题的 `message_thread_id`,相当于 Discord 的子区)可选。不存在默认频道回退。
其他路由字段:
@ -102,10 +107,13 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
{ "type": "event", "match": "pull_request" },
{ "type": "actor", "match": "[bot]", "exclude": true }
],
"target": {
"channelId": "1234567890",
"threadId": "9876543210"
}
"targets": [
{
"platform": "telegram",
"chatId": "-1001234567890",
"topicId": "9876543210"
}
]
}
]
```

View file

@ -31,6 +31,7 @@ npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put DISCORD_TOKEN
npx wrangler secret put DISCORD_PUBLIC_KEY # Discord 应用的公钥(开发者门户获取),交互功能必需
npx wrangler secret put TELEGRAM_TOKEN # Telegram Bot TokenBotFather 获取)—— Telegram 路由必需
npx wrangler secret put ADMIN_USER_IDS # 逗号分隔的 GitHub ID/登录名,允许进入 Web UI
```