mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +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);
|
emit("save", route);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
<span class="dot" :class="l.ok ? 'ok' : 'bad'"></span>
|
<span class="dot" :class="l.ok ? 'ok' : 'bad'"></span>
|
||||||
<span class="log-route">{{ l.routeId }}</span>
|
<span class="log-route">{{ l.routeId }}</span>
|
||||||
<span class="log-event">{{ l.event }}</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>
|
<span class="log-time">{{ fmtTime(l.ts) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-meta">
|
<div class="log-meta">
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,25 @@ WebHooker requires several secrets to function. For local development, store the
|
||||||
|
|
||||||
### Required Secrets
|
### Required Secrets
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ----------------------- | ----------------------------------------------- |
|
| ----------------------- | ------------------------------------------------------------------ |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||||||
| `GITHUB_APP_ID` | Numeric ID of your GitHub App |
|
| `GITHUB_APP_ID` | Numeric ID of your GitHub App |
|
||||||
| `GITHUB_PRIVATE_KEY` | App private key (PEM format, with `\n` escapes) |
|
| `GITHUB_PRIVATE_KEY` | App private key (PEM format, with `\n` escapes) |
|
||||||
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
||||||
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
|
| `GITHUB_CLIENT_SECRET` | OAuth client secret from App settings |
|
||||||
| `DISCORD_TOKEN` | Discord bot token |
|
| `DISCORD_TOKEN` | Discord bot token |
|
||||||
| `TELEGRAM_TOKEN` | Telegram bot token (from BotFather) — required for Telegram routes |
|
| `TELEGRAM_TOKEN` | Telegram bot token (from BotFather) — required for Telegram routes |
|
||||||
|
|
||||||
### Optional Secrets
|
### Optional Secrets
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| ------------------------ | ----------------------------------------------------------------------------- | --------------------------------- |
|
| ------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------- |
|
||||||
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
|
| `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 |
|
| `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) |
|
| `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` |
|
| `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 |
|
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
|
||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,25 @@ WebHooker 需要多个密钥才能运行。本地开发时存储在 `.dev.vars`
|
||||||
|
|
||||||
### 必需密钥
|
### 必需密钥
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
| ----------------------- | ---------------------------------- |
|
| ----------------------- | -------------------------------------------------------- |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 Webhook 密钥 |
|
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 Webhook 密钥 |
|
||||||
| `GITHUB_APP_ID` | GitHub App 的数字 ID |
|
| `GITHUB_APP_ID` | GitHub App 的数字 ID |
|
||||||
| `GITHUB_PRIVATE_KEY` | App 私钥(PEM 格式,用 `\n` 转义) |
|
| `GITHUB_PRIVATE_KEY` | App 私钥(PEM 格式,用 `\n` 转义) |
|
||||||
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
||||||
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
||||||
| `DISCORD_TOKEN` | Discord Bot Token |
|
| `DISCORD_TOKEN` | Discord Bot Token |
|
||||||
| `TELEGRAM_TOKEN` | Telegram Bot Token(BotFather 获取)—— Telegram 路由必需 |
|
| `TELEGRAM_TOKEN` | Telegram Bot Token(BotFather 获取)—— Telegram 路由必需 |
|
||||||
|
|
||||||
### 可选密钥
|
### 可选密钥
|
||||||
|
|
||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
| ------------------------ | ------------------------------------------------------ | ----------------------- |
|
| ------------------------- | -------------------------------------------------------------------- | ----------------------- |
|
||||||
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
|
| `BASE_URL` | OAuth 回调的公开 URL | `http://localhost:8787` |
|
||||||
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
||||||
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
|
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
|
||||||
| `DISCORD_APPLICATION_ID` | Discord 应用 ID;省略时自动获取 | 自动获取 |
|
| `DISCORD_APPLICATION_ID` | Discord 应用 ID;省略时自动获取 | 自动获取 |
|
||||||
| `TELEGRAM_WEBHOOK_SECRET`| `POST /telegram/webhook` 验签密钥(X-Telegram-Bot-Api-Secret-Token) | 未设置时不校验 |
|
| `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥(X-Telegram-Bot-Api-Secret-Token) | 未设置时不校验 |
|
||||||
|
|
||||||
## Web 控制台
|
## Web 控制台
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,18 @@ function createMockDB(): D1Database {
|
||||||
prepare: (sql: string) => ({
|
prepare: (sql: string) => ({
|
||||||
bind: (...args: unknown[]) => ({
|
bind: (...args: unknown[]) => ({
|
||||||
run: async (): Promise<{ success: boolean }> => {
|
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]));
|
if (m) links.set(String(args[0]), String(args[1]));
|
||||||
const del = sql.match(/DELETE FROM telegram_links WHERE telegram_user_id = \?/);
|
const del = sql.match(/DELETE FROM telegram_links WHERE telegram_user_id = \?/);
|
||||||
if (del) links.delete(String(args[0]));
|
if (del) links.delete(String(args[0]));
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
all: async (): Promise<{ results: Array<Record<string, unknown>> }> => {
|
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) {
|
if (sel) {
|
||||||
const val = links.get(String(args[0]));
|
const val = links.get(String(args[0]));
|
||||||
return { results: val ? [{ github_user_id: val }] : [] };
|
return { results: val ? [{ github_user_id: val }] : [] };
|
||||||
|
|
@ -146,7 +150,9 @@ describe("telegram-commands /gh comment", () => {
|
||||||
mockFetch((url, init) => {
|
mockFetch((url, init) => {
|
||||||
calls.push({ url, body: String(init!.body) });
|
calls.push({ url, body: String(init!.body) });
|
||||||
if (String(url).endsWith("/sendMessage")) {
|
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(
|
return new Response(
|
||||||
JSON.stringify({ html_url: "https://github.com/acme/widget/issues/7#issuecomment-9" }),
|
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 () => {
|
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 env = createEnv();
|
||||||
const res = await handleTelegramWebhookRequest(
|
const res = await handleTelegramWebhookRequest(
|
||||||
new Request("https://example.com/telegram/webhook", {
|
new Request("https://example.com/telegram/webhook", {
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,16 @@ describe("telegram renderNeutralMessage", () => {
|
||||||
footer: "acme/widget",
|
footer: "acme/widget",
|
||||||
};
|
};
|
||||||
const out = renderNeutralMessage(message);
|
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("<b>Status</b>: success");
|
||||||
expect(out).toContain("<i>acme/widget</i>");
|
expect(out).toContain("<i>acme/widget</i>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("escapes HTML special characters", () => {
|
it("escapes HTML special characters", () => {
|
||||||
const out = renderNeutralMessage({
|
const out = renderNeutralMessage({
|
||||||
title: "a <b> & \"c\"",
|
title: 'a <b> & "c"',
|
||||||
fields: [{ name: "body", value: "<script>alert(1)</script>" }],
|
fields: [{ name: "body", value: "<script>alert(1)</script>" }],
|
||||||
});
|
});
|
||||||
expect(out).not.toContain("<b>acme");
|
expect(out).not.toContain("<b>acme");
|
||||||
|
|
@ -76,8 +78,9 @@ describe("telegram-rest sendMessage", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns error on non-ok response", async () => {
|
it("returns error on non-ok response", async () => {
|
||||||
mockFetch(() =>
|
mockFetch(
|
||||||
new Response(JSON.stringify({ ok: false, description: "chat not found" }), { status: 400 }),
|
() =>
|
||||||
|
new Response(JSON.stringify({ ok: false, description: "chat not found" }), { status: 400 }),
|
||||||
);
|
);
|
||||||
const result = await sendMessage("t", "-100123", "hello");
|
const result = await sendMessage("t", "-100123", "hello");
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
target: targetStr,
|
target: targetStr,
|
||||||
deliveryId: event.deliveryId,
|
deliveryId: event.deliveryId,
|
||||||
actor: (event.payload.sender as { login?: string } | undefined)?.login,
|
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();
|
const started = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,12 @@ function extractTarget(msg: TelegramMessage, prOnly = false): Target | null {
|
||||||
return 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);
|
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 topicId = msg.message_thread_id != null ? String(msg.message_thread_id) : undefined;
|
||||||
|
|
||||||
const clientId = env.GITHUB_CLIENT_ID;
|
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, "");
|
const state = crypto.randomUUID().replace(/-/g, "");
|
||||||
await env.KV.put(
|
await env.KV.put(
|
||||||
|
|
@ -87,7 +93,12 @@ async function cmdLogin(env: Env, msg: TelegramMessage): Promise<void> {
|
||||||
{ expirationTtl: 600 },
|
{ expirationTtl: 600 },
|
||||||
);
|
);
|
||||||
const url = getOAuthURL(clientId, state);
|
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> {
|
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);
|
await closePullRequestAsUser(env.KV, githubUserId, target.owner, target.repo, target.number);
|
||||||
}
|
}
|
||||||
const label = op === "merge" ? "合并" : "关闭";
|
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) {
|
} catch (err) {
|
||||||
await reply(env, chatId, topicId, errText(err));
|
await reply(env, chatId, topicId, errText(err));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,10 @@ export async function sendMessage(
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
lastError = data?.description ?? `HTTP ${res.status}`;
|
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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: lastError,
|
error: lastError,
|
||||||
|
|
@ -72,11 +75,23 @@ export async function sendMessage(
|
||||||
lastError = err instanceof Error ? err.message : String(err);
|
lastError = err instanceof Error ? err.message : String(err);
|
||||||
log.error({ err, chatId, attempts: attempt + 1 }, "Failed to send Telegram message");
|
log.error({ err, chatId, attempts: attempt + 1 }, "Failed to send Telegram message");
|
||||||
if (attempt === 2) {
|
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)));
|
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,
|
githubUserId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db
|
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)
|
.bind(telegramUserId, githubUserId)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -121,5 +123,8 @@ export async function getTelegramLink(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeTelegramLink(db: D1Database, telegramUserId: string): Promise<void> {
|
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