mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
chore: auto-fix lint & formatting [skip ci]
This commit is contained in:
parent
bd7a8f2632
commit
568e206643
10 changed files with 98 additions and 50 deletions
|
|
@ -312,5 +312,4 @@ function save(): void {
|
|||
}
|
||||
emit("save", route);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@
|
|||
<span class="dot" :class="l.ok ? 'ok' : 'bad'"></span>
|
||||
<span class="log-route">{{ l.routeId }}</span>
|
||||
<span class="log-event">{{ l.event }}</span>
|
||||
<span v-if="l.status" class="log-status" :class="l.ok ? 'ok' : 'bad'">{{ l.status }}</span>
|
||||
<span v-if="l.status" class="log-status" :class="l.ok ? 'ok' : 'bad'">{{
|
||||
l.status
|
||||
}}</span>
|
||||
<span class="log-time">{{ fmtTime(l.ts) }}</span>
|
||||
</div>
|
||||
<div class="log-meta">
|
||||
|
|
|
|||
|
|
@ -6,25 +6,25 @@ WebHooker requires several secrets to function. For local development, store the
|
|||
|
||||
### Required Secrets
|
||||
|
||||
| Variable | Description |
|
||||
| ----------------------- | ----------------------------------------------- |
|
||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||||
| `GITHUB_APP_ID` | Numeric ID of your GitHub App |
|
||||
| `GITHUB_PRIVATE_KEY` | App private key (PEM format, with `\n` escapes) |
|
||||
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
||||
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
|
||||
| `DISCORD_TOKEN` | Discord bot token |
|
||||
| Variable | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------ |
|
||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||||
| `GITHUB_APP_ID` | Numeric ID of your GitHub App |
|
||||
| `GITHUB_PRIVATE_KEY` | App private key (PEM format, with `\n` escapes) |
|
||||
| `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
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
## Web UI
|
||||
|
||||
|
|
|
|||
|
|
@ -6,25 +6,25 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
|
|||
|
||||
### 必需密钥
|
||||
|
||||
| 变量 | 说明 |
|
||||
| ----------------------- | ---------------------------------- |
|
||||
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 Webhook 密钥 |
|
||||
| `GITHUB_APP_ID` | GitHub App 的数字 ID |
|
||||
| `GITHUB_PRIVATE_KEY` | App 私钥(PEM 格式,用 `\n` 转义) |
|
||||
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
||||
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
||||
| `DISCORD_TOKEN` | Discord Bot Token |
|
||||
| 变量 | 说明 |
|
||||
| ----------------------- | -------------------------------------------------------- |
|
||||
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 Webhook 密钥 |
|
||||
| `GITHUB_APP_ID` | GitHub App 的数字 ID |
|
||||
| `GITHUB_PRIVATE_KEY` | App 私钥(PEM 格式,用 `\n` 转义) |
|
||||
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
||||
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
||||
| `DISCORD_TOKEN` | Discord Bot Token |
|
||||
| `TELEGRAM_TOKEN` | Telegram Bot Token(BotFather 获取)—— Telegram 路由必需 |
|
||||
|
||||
### 可选密钥
|
||||
|
||||
| 变量 | 说明 | 默认值 |
|
||||
| ------------------------ | ------------------------------------------------------ | ----------------------- |
|
||||
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
|
||||
| `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) | 未设置时不校验 |
|
||||
| 变量 | 说明 | 默认值 |
|
||||
| ------------------------- | -------------------------------------------------------------------- | ----------------------- |
|
||||
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
|
||||
| `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 控制台
|
||||
|
||||
|
|
|
|||
|
|
@ -38,14 +38,18 @@ function createMockDB(): D1Database {
|
|||
prepare: (sql: string) => ({
|
||||
bind: (...args: unknown[]) => ({
|
||||
run: async (): Promise<{ success: boolean }> => {
|
||||
const m = sql.match(/INSERT OR REPLACE INTO telegram_links \(telegram_user_id, github_user_id\) VALUES \(\?, \?\)/);
|
||||
const m = sql.match(
|
||||
/INSERT OR REPLACE INTO telegram_links \(telegram_user_id, github_user_id\) VALUES \(\?, \?\)/,
|
||||
);
|
||||
if (m) links.set(String(args[0]), String(args[1]));
|
||||
const del = sql.match(/DELETE FROM telegram_links WHERE telegram_user_id = \?/);
|
||||
if (del) links.delete(String(args[0]));
|
||||
return { success: true };
|
||||
},
|
||||
all: async (): Promise<{ results: Array<Record<string, unknown>> }> => {
|
||||
const sel = sql.match(/SELECT github_user_id FROM telegram_links WHERE telegram_user_id = \?/);
|
||||
const sel = sql.match(
|
||||
/SELECT github_user_id FROM telegram_links WHERE telegram_user_id = \?/,
|
||||
);
|
||||
if (sel) {
|
||||
const val = links.get(String(args[0]));
|
||||
return { results: val ? [{ github_user_id: val }] : [] };
|
||||
|
|
@ -146,7 +150,9 @@ describe("telegram-commands /gh comment", () => {
|
|||
mockFetch((url, init) => {
|
||||
calls.push({ url, body: String(init!.body) });
|
||||
if (String(url).endsWith("/sendMessage")) {
|
||||
return new Response(JSON.stringify({ ok: true, result: { message_id: 1 } }), { status: 200 });
|
||||
return new Response(JSON.stringify({ ok: true, result: { message_id: 1 } }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({ html_url: "https://github.com/acme/widget/issues/7#issuecomment-9" }),
|
||||
|
|
@ -185,7 +191,9 @@ describe("telegram-updates webhook", () => {
|
|||
});
|
||||
|
||||
it("accepts requests with the correct secret token", async () => {
|
||||
mockFetch(() => new Response(JSON.stringify({ ok: true, result: { message_id: 1 } }), { status: 200 }));
|
||||
mockFetch(
|
||||
() => new Response(JSON.stringify({ ok: true, result: { message_id: 1 } }), { status: 200 }),
|
||||
);
|
||||
const env = createEnv();
|
||||
const res = await handleTelegramWebhookRequest(
|
||||
new Request("https://example.com/telegram/webhook", {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,16 @@ describe("telegram renderNeutralMessage", () => {
|
|||
footer: "acme/widget",
|
||||
};
|
||||
const out = renderNeutralMessage(message);
|
||||
expect(out).toContain('<b><a href="https://github.com/acme/widget">acme/widget: Add feature</a></b>');
|
||||
expect(out).toContain(
|
||||
'<b><a href="https://github.com/acme/widget">acme/widget: Add feature</a></b>',
|
||||
);
|
||||
expect(out).toContain("<b>Status</b>: success");
|
||||
expect(out).toContain("<i>acme/widget</i>");
|
||||
});
|
||||
|
||||
it("escapes HTML special characters", () => {
|
||||
const out = renderNeutralMessage({
|
||||
title: "a <b> & \"c\"",
|
||||
title: 'a <b> & "c"',
|
||||
fields: [{ name: "body", value: "<script>alert(1)</script>" }],
|
||||
});
|
||||
expect(out).not.toContain("<b>acme");
|
||||
|
|
@ -76,8 +78,9 @@ describe("telegram-rest sendMessage", () => {
|
|||
});
|
||||
|
||||
it("returns error on non-ok response", async () => {
|
||||
mockFetch(() =>
|
||||
new Response(JSON.stringify({ ok: false, description: "chat not found" }), { status: 400 }),
|
||||
mockFetch(
|
||||
() =>
|
||||
new Response(JSON.stringify({ ok: false, description: "chat not found" }), { status: 400 }),
|
||||
);
|
||||
const result = await sendMessage("t", "-100123", "hello");
|
||||
expect(result.ok).toBe(false);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
|||
target: targetStr,
|
||||
deliveryId: event.deliveryId,
|
||||
actor: (event.payload.sender as { login?: string } | undefined)?.login,
|
||||
action: (event.payload.action as string | undefined),
|
||||
action: event.payload.action as string | undefined,
|
||||
};
|
||||
|
||||
const started = Date.now();
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@ function extractTarget(msg: TelegramMessage, prOnly = false): Target | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function reply(env: Env, chatId: string, topicId: string | undefined, text: string): Promise<void> {
|
||||
async function reply(
|
||||
env: Env,
|
||||
chatId: string,
|
||||
topicId: string | undefined,
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
await sendMessage(env.TELEGRAM_TOKEN ?? "", chatId, text, topicId);
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +78,8 @@ async function cmdLogin(env: Env, msg: TelegramMessage): Promise<void> {
|
|||
const topicId = msg.message_thread_id != null ? String(msg.message_thread_id) : undefined;
|
||||
|
||||
const clientId = env.GITHUB_CLIENT_ID;
|
||||
if (!clientId) return reply(env, chatId, topicId, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。");
|
||||
if (!clientId)
|
||||
return reply(env, chatId, topicId, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。");
|
||||
|
||||
const state = crypto.randomUUID().replace(/-/g, "");
|
||||
await env.KV.put(
|
||||
|
|
@ -87,7 +93,12 @@ async function cmdLogin(env: Env, msg: TelegramMessage): Promise<void> {
|
|||
{ expirationTtl: 600 },
|
||||
);
|
||||
const url = getOAuthURL(clientId, state);
|
||||
await reply(env, chatId, topicId, `点击链接授权 GitHub,即可用**本人身份**评论(10 分钟内有效):\n${url}`);
|
||||
await reply(
|
||||
env,
|
||||
chatId,
|
||||
topicId,
|
||||
`点击链接授权 GitHub,即可用**本人身份**评论(10 分钟内有效):\n${url}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function cmdLogout(env: Env, msg: TelegramMessage): Promise<void> {
|
||||
|
|
@ -165,7 +176,12 @@ async function cmdMergeClose(env: Env, msg: TelegramMessage, op: "merge" | "clos
|
|||
await closePullRequestAsUser(env.KV, githubUserId, target.owner, target.repo, target.number);
|
||||
}
|
||||
const label = op === "merge" ? "合并" : "关闭";
|
||||
await reply(env, chatId, topicId, `✅ 已${label} PR ${target.owner}/${target.repo}#${target.number}`);
|
||||
await reply(
|
||||
env,
|
||||
chatId,
|
||||
topicId,
|
||||
`✅ 已${label} PR ${target.owner}/${target.repo}#${target.number}`,
|
||||
);
|
||||
} catch (err) {
|
||||
await reply(env, chatId, topicId, errText(err));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ export async function sendMessage(
|
|||
|
||||
if (!res.ok) {
|
||||
lastError = data?.description ?? `HTTP ${res.status}`;
|
||||
log.error({ status: res.status, err: lastError, chatId, attempts: attempt + 1 }, "Telegram API error");
|
||||
log.error(
|
||||
{ status: res.status, err: lastError, chatId, attempts: attempt + 1 },
|
||||
"Telegram API error",
|
||||
);
|
||||
return {
|
||||
ok: false,
|
||||
error: lastError,
|
||||
|
|
@ -72,11 +75,23 @@ export async function sendMessage(
|
|||
lastError = err instanceof Error ? err.message : String(err);
|
||||
log.error({ err, chatId, attempts: attempt + 1 }, "Failed to send Telegram message");
|
||||
if (attempt === 2) {
|
||||
return { ok: false, error: lastError, errorCode: "NETWORK", status: lastStatus, attempts: attempt + 1 };
|
||||
return {
|
||||
ok: false,
|
||||
error: lastError,
|
||||
errorCode: "NETWORK",
|
||||
status: lastStatus,
|
||||
attempts: attempt + 1,
|
||||
};
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: false, error: lastError || "Failed to send Telegram message", errorCode: "RETRIES", status: lastStatus, attempts: 3 };
|
||||
return {
|
||||
ok: false,
|
||||
error: lastError || "Failed to send Telegram message",
|
||||
errorCode: "RETRIES",
|
||||
status: lastStatus,
|
||||
attempts: 3,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@ export async function saveTelegramLink(
|
|||
githubUserId: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.prepare("INSERT OR REPLACE INTO telegram_links (telegram_user_id, github_user_id) VALUES (?, ?)")
|
||||
.prepare(
|
||||
"INSERT OR REPLACE INTO telegram_links (telegram_user_id, github_user_id) VALUES (?, ?)",
|
||||
)
|
||||
.bind(telegramUserId, githubUserId)
|
||||
.run();
|
||||
}
|
||||
|
|
@ -121,5 +123,8 @@ export async function getTelegramLink(
|
|||
}
|
||||
|
||||
export async function removeTelegramLink(db: D1Database, telegramUserId: string): Promise<void> {
|
||||
await db.prepare("DELETE FROM telegram_links WHERE telegram_user_id = ?").bind(telegramUserId).run();
|
||||
await db
|
||||
.prepare("DELETE FROM telegram_links WHERE telegram_user_id = ?")
|
||||
.bind(telegramUserId)
|
||||
.run();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue