diff --git a/README.md b/README.md index 87c6e51..f3d3baa 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,18 @@ npx wrangler dev # Start local dev server ### Secrets (`.dev.vars` for local, Worker Secrets for production) -| Variable | Description | -| ------------------------- | --------------------------------------------------------------------------------------------- | -| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub | -| `GITHUB_APP_ID` | GitHub App ID | -| `GITHUB_PRIVATE_KEY` | App private key (PEM) | -| `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) | -| `BASE_URL` | Public URL for OAuth callbacks | -| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` | +| Variable | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------- | +| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub | +| `GITHUB_APP_ID` | GitHub App ID | +| `GITHUB_PRIVATE_KEY` | App private key (PEM) | +| `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) | +| `BASE_URL` | Public URL for OAuth callbacks | +| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` | ### Routes @@ -211,12 +211,12 @@ The bot registers native **slash** and **message context-menu** commands, synced **Requirements:** -| Item | How | -| ---------------- | ----------------------------------------------------------------- | -| Public key | `DISCORD_PUBLIC_KEY` set + Interactions Endpoint URL configured | -| Invite scope | Bot invited with `applications.commands` (see invite URL above) | -| OAuth | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` and `BASE_URL` configured | -| User linked | Each user runs `/gh login` first | +| Item | How | +| ------------ | --------------------------------------------------------------------- | +| Public key | `DISCORD_PUBLIC_KEY` set + Interactions Endpoint URL configured | +| Invite scope | Bot invited with `applications.commands` (see invite URL above) | +| OAuth | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` and `BASE_URL` configured | +| User linked | Each user runs `/gh login` first | ## Deployment diff --git a/README.zh.md b/README.zh.md index 59d176d..553a8b7 100644 --- a/README.zh.md +++ b/README.zh.md @@ -42,18 +42,18 @@ npx wrangler dev # 启动本地开发服务器 ### 密钥(本地用 `.dev.vars`,生产用 Worker Secrets) -| 变量 | 说明 | -| ------------------------- | -------------------------------------------------------------------------- | -| `GITHUB_WEBHOOK_SECRET` | GitHub webhook 密钥 | -| `GITHUB_APP_ID` | GitHub App ID | -| `GITHUB_PRIVATE_KEY` | App 私钥(PEM) | -| `GITHUB_CLIENT_ID` | OAuth Client ID | -| `GITHUB_CLIENT_SECRET` | OAuth Client Secret | -| `DISCORD_TOKEN` | 机器人 token | -| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取)—— 交互功能必需 | -| `DISCORD_APPLICATION_ID` | Discord 应用 ID(可选;省略时通过 `GET /oauth2/applications/@me` 自动获取) | -| `BASE_URL` | 公网地址(用于 OAuth 回调) | -| `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID(或登录名),逗号分隔 | +| 变量 | 说明 | +| ------------------------ | --------------------------------------------------------------------------- | +| `GITHUB_WEBHOOK_SECRET` | GitHub webhook 密钥 | +| `GITHUB_APP_ID` | GitHub App ID | +| `GITHUB_PRIVATE_KEY` | App 私钥(PEM) | +| `GITHUB_CLIENT_ID` | OAuth Client ID | +| `GITHUB_CLIENT_SECRET` | OAuth Client Secret | +| `DISCORD_TOKEN` | 机器人 token | +| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取)—— 交互功能必需 | +| `DISCORD_APPLICATION_ID` | Discord 应用 ID(可选;省略时通过 `GET /oauth2/applications/@me` 自动获取) | +| `BASE_URL` | 公网地址(用于 OAuth 回调) | +| `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID(或登录名),逗号分隔 | ### 路由配置 @@ -210,12 +210,12 @@ bot 通过定时任务(每 5 分钟)同步注册原生的**斜杠命令**与 **要求:** -| 项目 | 说明 | -| ------------ | ---------------------------------------------------------------- | -| Public Key | 已配置 `DISCORD_PUBLIC_KEY` 且已设置 Interactions Endpoint URL | -| 邀请 scope | 邀请时带上 `applications.commands`(见上方邀请链接) | -| OAuth | 已配置 `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` 与 `BASE_URL` | -| 用户绑定 | 每个用户先执行 `/gh login` | +| 项目 | 说明 | +| ---------- | ---------------------------------------------------------------- | +| Public Key | 已配置 `DISCORD_PUBLIC_KEY` 且已设置 Interactions Endpoint URL | +| 邀请 scope | 邀请时带上 `applications.commands`(见上方邀请链接) | +| OAuth | 已配置 `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` 与 `BASE_URL` | +| 用户绑定 | 每个用户先执行 `/gh login` | ## 部署 diff --git a/docs/api/overview.md b/docs/api/overview.md index 9b446ec..696ebd2 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -10,27 +10,27 @@ https://your-worker.workers.dev ## Endpoints -| Method | Path | Auth | Description | -| -------- | ------------------------------ | -------------- | ------------------------ | -| `GET` | `/health` | None | Health check | -| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion | +| Method | Path | Auth | Description | +| -------- | ------------------------------ | ----------------- | ------------------------------------------------------ | +| `GET` | `/health` | None | Health check | +| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion | | `POST` | `/discord/interactions` | Ed25519 signature | Discord interactions (slash commands, buttons, modals) | -| `GET` | `/auth/github` | None | Start GitHub OAuth flow | -| `GET` | `/auth/github/callback` | None | OAuth callback | -| `DELETE` | `/auth/token/:userId` | None | Revoke user token | -| `POST` | `/api/comment` | Bearer token | Create issue comment | -| `POST` | `/api/merge` | Bearer token | Merge pull request | -| `POST` | `/api/close` | Bearer token | Close pull request | -| `POST` | `/api/react` | Bearer token | Add reaction to issue | -| `GET` | `/admin` | Admin session | Config console UI | -| `GET` | `/admin/api/routes` | Admin session | List routes | -| `PUT` | `/admin/api/routes` | Admin session | Replace routes | -| `GET` | `/admin/api/groups` | Admin session | List groups (scoped) | -| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super) | -| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes | -| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes | -| `GET` | `/admin/api/me` | Admin session | Current session info | -| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) | +| `GET` | `/auth/github` | None | Start GitHub OAuth flow | +| `GET` | `/auth/github/callback` | None | OAuth callback | +| `DELETE` | `/auth/token/:userId` | None | Revoke user token | +| `POST` | `/api/comment` | Bearer token | Create issue comment | +| `POST` | `/api/merge` | Bearer token | Merge pull request | +| `POST` | `/api/close` | Bearer token | Close pull request | +| `POST` | `/api/react` | Bearer token | Add reaction to issue | +| `GET` | `/admin` | Admin session | Config console UI | +| `GET` | `/admin/api/routes` | Admin session | List routes | +| `PUT` | `/admin/api/routes` | Admin session | Replace routes | +| `GET` | `/admin/api/groups` | Admin session | List groups (scoped) | +| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super) | +| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes | +| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes | +| `GET` | `/admin/api/me` | Admin session | Current session info | +| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) | ## Admin Console diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index dc28126..378fa1c 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -17,12 +17,12 @@ WebHooker requires several secrets to function. For local development, store the ### Optional Secrets -| Variable | Description | Default | -| ------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------- | -| `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 | -| `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 | +| Variable | Description | Default | +| ------------------------ | ----------------------------------------------------------------------------- | --------------------------------- | +| `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 | +| `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 | ## Web UI diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 57bff52..361055d 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -17,11 +17,11 @@ POST /discord/interactions → verify (Ed25519) → handle /gh slash & context c ### Components -| Component | Role | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, REST send | -| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) | -| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), route config (`config:routes`), send logs, delivery dedup | +| Component | Role | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, REST send | +| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) | +| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), route config (`config:routes`), send logs, delivery dedup | ### Data Flow diff --git a/docs/zh/api/overview.md b/docs/zh/api/overview.md index 5303adb..c64b949 100644 --- a/docs/zh/api/overview.md +++ b/docs/zh/api/overview.md @@ -10,27 +10,27 @@ https://your-worker.workers.dev ## 端点 -| 方法 | 路径 | 鉴权 | 说明 | -| -------- | ------------------------------ | ------------ | ------------------------ | -| `GET` | `/health` | 无 | 健康检查 | -| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 | +| 方法 | 路径 | 鉴权 | 说明 | +| -------- | ------------------------------ | ------------ | ------------------------------------- | +| `GET` | `/health` | 无 | 健康检查 | +| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 | | `POST` | `/discord/interactions` | Ed25519 签名 | Discord 交互(斜杠命令、按钮、modal) | -| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 | -| `GET` | `/auth/github/callback` | 无 | OAuth 回调 | -| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token | -| `POST` | `/api/comment` | Bearer Token | 创建议题评论 | -| `POST` | `/api/merge` | Bearer Token | 合并拉取请求 | -| `POST` | `/api/close` | Bearer Token | 关闭拉取请求 | -| `POST` | `/api/react` | Bearer Token | 添加议题反应 | -| `GET` | `/admin` | 管理员会话 | 配置控制台页面 | -| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由 | -| `PUT` | `/admin/api/routes` | 管理员会话 | 替换路由 | -| `GET` | `/admin/api/groups` | 管理员会话 | 列出分组(按权限过滤) | -| `PUT` | `/admin/api/groups` | 管理员会话 | 替换分组(仅超级管理员) | -| `GET` | `/admin/api/groups/:id/routes` | 管理员会话 | 列出某分组的路由 | -| `PUT` | `/admin/api/groups/:id/routes` | 管理员会话 | 替换某分组的路由 | -| `GET` | `/admin/api/me` | 管理员会话 | 当前会话信息 | -| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) | +| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 | +| `GET` | `/auth/github/callback` | 无 | OAuth 回调 | +| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token | +| `POST` | `/api/comment` | Bearer Token | 创建议题评论 | +| `POST` | `/api/merge` | Bearer Token | 合并拉取请求 | +| `POST` | `/api/close` | Bearer Token | 关闭拉取请求 | +| `POST` | `/api/react` | Bearer Token | 添加议题反应 | +| `GET` | `/admin` | 管理员会话 | 配置控制台页面 | +| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由 | +| `PUT` | `/admin/api/routes` | 管理员会话 | 替换路由 | +| `GET` | `/admin/api/groups` | 管理员会话 | 列出分组(按权限过滤) | +| `PUT` | `/admin/api/groups` | 管理员会话 | 替换分组(仅超级管理员) | +| `GET` | `/admin/api/groups/:id/routes` | 管理员会话 | 列出某分组的路由 | +| `PUT` | `/admin/api/groups/:id/routes` | 管理员会话 | 替换某分组的路由 | +| `GET` | `/admin/api/me` | 管理员会话 | 当前会话信息 | +| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) | ## 管理控制台 diff --git a/docs/zh/guide/configuration.md b/docs/zh/guide/configuration.md index e424154..29161a2 100644 --- a/docs/zh/guide/configuration.md +++ b/docs/zh/guide/configuration.md @@ -184,4 +184,4 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理 | `logs:send:{ts}-{hex}` | 发送记录 | 1 小时 | | `cmd:guild:{id}` | 已注册命令的服务器 id(去重标记) | 永久 | | `cmd:registered:global` | 全局命令已注册标记(24h 去重) | 1 天 | -| `config:discord-app-id` | Discord 应用 id 缓存 | 永久 | +| `config:discord-app-id` | Discord 应用 id 缓存 | 永久 | diff --git a/docs/zh/guide/introduction.md b/docs/zh/guide/introduction.md index 90d55df..ed0252c 100644 --- a/docs/zh/guide/introduction.md +++ b/docs/zh/guide/introduction.md @@ -17,11 +17,11 @@ POST /discord/interactions → 验证 (Ed25519) → 处理 /gh 斜杠与右键 ### 组件 -| 组件 | 职责 | -| ----------------------------------- | --------------------------------------------------------------------------------------------------------- | -| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、REST 发送 | -| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) | -| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、路由配置 (`config:routes`)、发送日志、投递去重 | +| 组件 | 职责 | +| ------------------------- | --------------------------------------------------------------------------------------------------------- | +| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、REST 发送 | +| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互(斜杠命令、右键菜单、按钮、modal) | +| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、路由配置 (`config:routes`)、发送日志、投递去重 | ### 数据流 diff --git a/src/discord-interactions.ts b/src/discord-interactions.ts index 218f267..79d740f 100644 --- a/src/discord-interactions.ts +++ b/src/discord-interactions.ts @@ -165,7 +165,10 @@ export async function handleInteractionRequest(request: Request, env: Env): Prom const signature = request.headers.get("X-Signature-Ed25519"); const timestamp = request.headers.get("X-Signature-Timestamp"); if (!signature || !timestamp || !env.DISCORD_PUBLIC_KEY) { - log.warn({ hasSig: !!signature, hasTs: !!timestamp, hasKey: !!env.DISCORD_PUBLIC_KEY }, "Discord interaction missing signature"); + log.warn( + { hasSig: !!signature, hasTs: !!timestamp, hasKey: !!env.DISCORD_PUBLIC_KEY }, + "Discord interaction missing signature", + ); return new Response("Invalid signature", { status: 401 }); } if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > TIMESTAMP_TOLERANCE_SECONDS) { @@ -293,7 +296,12 @@ async function respond(env: Env, id: string, token: string, content: string): Pr }); } -async function interactionCallback(env: Env, id: string, token: string, body: unknown): Promise { +async function interactionCallback( + env: Env, + id: string, + token: string, + body: unknown, +): Promise { const res = await fetch(`${DISCORD_API}/interactions/${id}/${token}/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -386,8 +394,7 @@ async function handleButton( async function cmdLogin(env: Env, id: string, token: string, userId: string | null): Promise { if (!userId) return respond(env, id, token, "无法识别你的 Discord 账号。"); const clientId = env.GITHUB_CLIENT_ID; - if (!clientId) - return respond(env, id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。"); + if (!clientId) return respond(env, id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。"); const state = crypto.randomUUID().replace(/-/g, ""); await env.KV.put( @@ -404,7 +411,12 @@ async function cmdLogin(env: Env, id: string, token: string, userId: string | nu ); } -async function cmdLogout(env: Env, id: string, token: string, userId: string | null): Promise { +async function cmdLogout( + env: Env, + id: string, + token: string, + userId: string | null, +): Promise { if (!userId) return respond(env, id, token, "无法识别你的 Discord 账号。"); await removeDiscordLink(env.KV, userId); await respond(env, id, token, "已解绑你的 GitHub 账号。"); @@ -413,8 +425,7 @@ async function cmdLogout(env: Env, id: string, token: string, userId: string | n /** Map a GitHub op error code to a user-facing (Chinese) message. */ function errText(err: unknown): string { const t = err instanceof Error ? err.message : String(err); - if (t === "GITHUB_TOKEN_EXPIRED") - return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。"; + if (t === "GITHUB_TOKEN_EXPIRED") return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。"; if (t === "GITHUB_FORBIDDEN") return "GitHub 拒绝了此操作:你的账号没有权限修改/删除这条评论。"; if (t === "GITHUB_NOT_FOUND") return "找不到目标(可能评论已被删除或仓库不可访问)。"; return `操作失败:${t}`; @@ -480,13 +491,7 @@ async function commentOp( // edit: fetch current body to prefill the modal. let prefill = ""; try { - const { body } = await getCommentAsUser( - env.KV, - githubUserId, - owner!, - repo!, - Number(commentId), - ); + const { body } = await getCommentAsUser(env.KV, githubUserId, owner!, repo!, Number(commentId)); prefill = body; } catch (err) { return respond(env, id, token, errText(err)); @@ -678,14 +683,11 @@ export async function syncGuildCommands(env: Env): Promise { for (const guild of guilds) { try { if (await env.KV.get(`cmd:guild:${guild.id}`)) continue; - const r = await fetch( - `${DISCORD_API}/applications/${appId}/guilds/${guild.id}/commands`, - { - method: "PUT", - headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify(APP_COMMANDS), - }, - ); + const r = await fetch(`${DISCORD_API}/applications/${appId}/guilds/${guild.id}/commands`, { + method: "PUT", + headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify(APP_COMMANDS), + }); if (r.ok) { await env.KV.put(`cmd:guild:${guild.id}`, "1"); log.info({ guildId: guild.id }, "Registered guild application commands");