diff --git a/admin/assets/css/main.css b/admin/assets/css/main.css index 1fefcb3..70afb4f 100644 --- a/admin/assets/css/main.css +++ b/admin/assets/css/main.css @@ -167,7 +167,11 @@ header { font-weight: 600; cursor: pointer; text-decoration: none; - transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s; + transition: + border-color 0.15s, + background 0.15s, + color 0.15s, + transform 0.1s; } .btn:hover { @@ -279,7 +283,9 @@ main { border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; - transition: border-color 0.15s, transform 0.15s; + transition: + border-color 0.15s, + transform 0.15s; animation: rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both; } @@ -407,7 +413,9 @@ main { border: 1px solid var(--border-strong); border-radius: 999px; cursor: pointer; - transition: background 0.18s, border-color 0.18s; + transition: + background 0.18s, + border-color 0.18s; } .switch .track::after { @@ -620,7 +628,9 @@ main { color: var(--text); font-family: var(--ui); font-size: 13.5px; - transition: border-color 0.15s, box-shadow 0.15s; + transition: + border-color 0.15s, + box-shadow 0.15s; } .field input[type="text"]:focus { @@ -733,7 +743,9 @@ main { .toast-enter-active, .toast-leave-active { - transition: opacity 0.22s, transform 0.22s; + transition: + opacity 0.22s, + transform 0.22s; } .toast-enter-from, @@ -782,7 +794,9 @@ main { font-size: 13px; font-weight: 600; cursor: pointer; - transition: background 0.15s, color 0.15s; + transition: + background 0.15s, + color 0.15s; } .tab:hover { diff --git a/admin/components/GroupEditor.vue b/admin/components/GroupEditor.vue index 6928dcb..30505dc 100644 --- a/admin/components/GroupEditor.vue +++ b/admin/components/GroupEditor.vue @@ -13,7 +13,12 @@
- +
@@ -28,19 +33,35 @@
- - + +
{{ t("groupEditor.adminsHint") }}
- - + +
{{ t("groupEditor.ownersHint") }}
{{ formError }}
- + diff --git a/admin/components/RouteCard.vue b/admin/components/RouteCard.vue index 2238a67..e37e4d6 100644 --- a/admin/components/RouteCard.vue +++ b/admin/components/RouteCard.vue @@ -12,20 +12,38 @@ {{ t("route.fallback") }}
- - + +
- {{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }} + {{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }} {{ fmtMatch(f.match) }} - {{ t("route.noFilters") }} + {{ t("route.noFilters") }}
- {{ t("route.channel") }}{{ route.target.channelId }} - {{ t("route.thread") }}{{ route.target.threadId }} + {{ t("route.channel") }}{{ route.target.channelId }} + {{ t("route.thread") }}{{ route.target.threadId }}
diff --git a/admin/components/RouteEditor.vue b/admin/components/RouteEditor.vue index f66dc2f..d5a69bd 100644 --- a/admin/components/RouteEditor.vue +++ b/admin/components/RouteEditor.vue @@ -12,7 +12,12 @@
- +
@@ -22,7 +27,11 @@
- +
{{ t("routeEditor.langHint") }}
@@ -32,38 +41,70 @@
- {{ t("routeEditor.fallback") }} {{ t("routeEditor.fallbackHint") }} + {{ t("routeEditor.fallback") }} + {{ t("routeEditor.fallbackHint") }}
- +
- + - +
- +
{{ filterError }}
- +
- - + +
{{ formError }}
- - + +
@@ -131,11 +172,12 @@ watch( form.fallback = r?.fallback ?? false; form.channelId = r?.target.channelId ?? ""; form.threadId = r?.target.threadId ?? ""; - form.filters = (r && r.filters.length - ? r.filters - : form.fallback - ? [] - : [{ type: "event", match: "", exclude: false }] + form.filters = ( + r && r.filters.length + ? r.filters + : form.fallback + ? [] + : [{ type: "event", match: "", exclude: false }] ).map((f) => ({ ...f, matchText: fmtMatch(f.match) })) as FilterForm[]; filterError.value = ""; formError.value = ""; diff --git a/admin/components/SendLogs.vue b/admin/components/SendLogs.vue index 63c6688..1575a3c 100644 --- a/admin/components/SendLogs.vue +++ b/admin/components/SendLogs.vue @@ -2,7 +2,9 @@
{{ t("logs.lastSends", { n: logs.length }) }} - +

{{ error }}

@@ -17,8 +19,14 @@ {{ fmtTime(l.ts) }}
- {{ t("logs.repo") }}{{ l.repo }} - {{ t("logs.target") }}{{ l.target }} + {{ t("logs.repo") }}{{ l.repo }} + {{ t("logs.target") }}{{ l.target }}
{{ l.error }}
diff --git a/admin/composables/useI18n.ts b/admin/composables/useI18n.ts index d1094de..2090e98 100644 --- a/admin/composables/useI18n.ts +++ b/admin/composables/useI18n.ts @@ -192,7 +192,8 @@ const zh: Dict = { "groupEditor.owners": "来源限定", "groupEditor.ownersNote": "(GitHub 组织 / 用户登录名,逗号分隔)", "groupEditor.ownersPlaceholder": "my-org, some-user", - "groupEditor.ownersHint": "只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。", + "groupEditor.ownersHint": + "只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。", "groupEditor.cancel": "取消", "groupEditor.save": "保存分组", "groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线", diff --git a/admin/pages/admin.vue b/admin/pages/admin.vue index 09dca01..f8f16f5 100644 --- a/admin/pages/admin.vue +++ b/admin/pages/admin.vue @@ -112,7 +112,11 @@ {{ g.id }}
- diff --git a/src/__tests__/discord.test.ts b/src/__tests__/discord.test.ts index b70538f..f924192 100644 --- a/src/__tests__/discord.test.ts +++ b/src/__tests__/discord.test.ts @@ -3,9 +3,7 @@ import { sendMessage } from "../discord-rest"; import { dispatchEvent, isGatewayEnabled } from "../discord"; import type { Env, Route } from "../types"; -function mockFetch( - handler: (url: string, init?: RequestInit) => Response, -): void { +function mockFetch(handler: (url: string, init?: RequestInit) => Response): void { globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit): Promise => Promise.resolve(handler(String(input), init)); } diff --git a/src/__tests__/send-log.test.ts b/src/__tests__/send-log.test.ts index 084aadc..06a0d8c 100644 --- a/src/__tests__/send-log.test.ts +++ b/src/__tests__/send-log.test.ts @@ -21,7 +21,11 @@ function createMockKV(): KVNamespace { delete: async (key: string) => { store.delete(key); }, - list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }), + list: async () => ({ + keys: [...store.keys()].map((k) => ({ name: k })), + list_complete: true, + cacheStatus: null, + }), } as unknown as KVNamespace; } diff --git a/src/__tests__/token-store.test.ts b/src/__tests__/token-store.test.ts index b5cc9d6..fd872d3 100644 --- a/src/__tests__/token-store.test.ts +++ b/src/__tests__/token-store.test.ts @@ -21,7 +21,11 @@ function createMockKV(): KVNamespace { delete: async (key: string) => { store.delete(key); }, - list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }), + list: async () => ({ + keys: [...store.keys()].map((k) => ({ name: k })), + list_complete: true, + cacheStatus: null, + }), } as unknown as KVNamespace; } diff --git a/src/action-routes.ts b/src/action-routes.ts index 9766f96..566cc25 100644 --- a/src/action-routes.ts +++ b/src/action-routes.ts @@ -144,7 +144,16 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> { if (!userId) return c.json({ error: "Invalid or expired token" }, 401); const body = await readJson(c); - const reactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] as const; + const reactions = [ + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes", + ] as const; if ( !body || !isNonEmptyString(body.owner) || @@ -164,14 +173,7 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> { repo: body.repo, issue_number: body.issueNumber, content: body.reaction as - | "+1" - | "-1" - | "laugh" - | "confused" - | "heart" - | "hooray" - | "rocket" - | "eyes", + "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes", }); } catch (err) { log.error({ err }, "Failed to create reaction"); diff --git a/src/admin-routes.ts b/src/admin-routes.ts index 44f87d0..e906145 100644 --- a/src/admin-routes.ts +++ b/src/admin-routes.ts @@ -7,13 +7,7 @@ import { clearAdminCookie, type AdminSession, } from "./admin-session"; -import { - loadGroups, - saveGroups, - resolveScope, - hasAnyAccess, - type AccessScope, -} from "./groups"; +import { loadGroups, saveGroups, resolveScope, hasAnyAccess, type AccessScope } from "./groups"; import { getSendLog } from "./send-log"; import { log } from "./log"; @@ -40,9 +34,7 @@ function deepEqual(a: unknown, b: unknown): boolean { const ak = Object.keys(ao); const bk = Object.keys(bo); if (ak.length !== bk.length) return false; - return ak.every( - (k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]), - ); + return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])); } return false; } @@ -314,7 +306,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> { const groupId = c.req.param("groupId"); const access = groupAccess(s, groupId); if (!access.ok) { - return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status); + return c.json( + { error: access.status === 404 ? "Group not found" : "Forbidden" }, + access.status, + ); } const all = await loadRoutes(c.env.KV); return c.json({ group: access.group, routes: all.filter((r) => r.groupId === groupId) }); @@ -326,7 +321,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> { const groupId = c.req.param("groupId"); const access = groupAccess(s, groupId); if (!access.ok) { - return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status); + return c.json( + { error: access.status === 404 ? "Group not found" : "Forbidden" }, + access.status, + ); } let body: unknown; diff --git a/src/discord-gateway.ts b/src/discord-gateway.ts index bee902b..b6c9ad8 100644 --- a/src/discord-gateway.ts +++ b/src/discord-gateway.ts @@ -50,7 +50,11 @@ const APP_COMMANDS = [ type: COMMAND_TYPE.CHAT_INPUT, description: "GitHub 集成", options: [ - { type: OPTION_TYPE.SUB_COMMAND, name: "login", description: "绑定你的 GitHub 账号以用本人身份评论" }, + { + type: OPTION_TYPE.SUB_COMMAND, + name: "login", + description: "绑定你的 GitHub 账号以用本人身份评论", + }, { type: OPTION_TYPE.SUB_COMMAND, name: "logout", description: "解绑你的 GitHub 账号" }, { type: OPTION_TYPE.SUB_COMMAND_GROUP, @@ -62,7 +66,12 @@ const APP_COMMANDS = [ name: "add", description: "在 issue/PR 下新增评论", options: [ - { type: OPTION_TYPE.STRING, name: "link", description: "issue/PR 链接", required: true }, + { + type: OPTION_TYPE.STRING, + name: "link", + description: "issue/PR 链接", + required: true, + }, ], }, { @@ -70,7 +79,12 @@ const APP_COMMANDS = [ name: "edit", description: "编辑一条评论", options: [ - { type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-)", required: true }, + { + type: OPTION_TYPE.STRING, + name: "link", + description: "评论链接(含 #issuecomment-)", + required: true, + }, ], }, { @@ -78,7 +92,12 @@ const APP_COMMANDS = [ name: "del", description: "删除一条评论", options: [ - { type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-)", required: true }, + { + type: OPTION_TYPE.STRING, + name: "link", + description: "评论链接(含 #issuecomment-)", + required: true, + }, ], }, ], @@ -186,7 +205,10 @@ export class DiscordGateway { }); ws.addEventListener("error", (event) => { - log.error({ err: String((event as ErrorEvent).message ?? event) }, "Gateway WebSocket error"); + log.error( + { err: String((event as ErrorEvent).message ?? event) }, + "Gateway WebSocket error", + ); }); } catch (err) { this.connecting = false; @@ -196,10 +218,7 @@ export class DiscordGateway { } private scheduleReconnect(): void { - const delay = Math.min( - BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt, - MAX_RECONNECT_DELAY, - ); + const delay = Math.min(BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt, MAX_RECONNECT_DELAY); this.reconnectAttempt++; this.state.storage.setAlarm(Date.now() + delay); } @@ -277,7 +296,10 @@ export class DiscordGateway { private identify(): void { if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.token) { - log.warn({ hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null }, "Cannot identify"); + log.warn( + { hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null }, + "Cannot identify", + ); return; } log.info("Sending IDENTIFY"); @@ -374,7 +396,14 @@ export class DiscordGateway { custom_id?: string; message?: { id?: string }; }; - return this.handleButton(id, token, userId, interaction.channel_id, data.message?.id, data.custom_id); + return this.handleButton( + id, + token, + userId, + interaction.channel_id, + data.message?.id, + data.custom_id, + ); } if (interaction.type === INTERACTION_TYPE.COMMAND) { @@ -382,14 +411,29 @@ export class DiscordGateway { name?: string; type?: number; target_id?: string; - options?: Array<{ name: string; options?: Array<{ name: string; value?: string; options?: Array<{ name: string; value?: string }> }> }>; - resolved?: { messages?: Record; content?: string }> }; + options?: Array<{ + name: string; + options?: Array<{ + name: string; + value?: string; + options?: Array<{ name: string; value?: string }>; + }>; + }>; + resolved?: { + messages?: Record; content?: string }>; + }; }; // Right-click (message context-menu) commands. if (data.type === COMMAND_TYPE.MESSAGE) { const op = - data.name === MSG_CMD_ADD ? "add" : data.name === MSG_CMD_EDIT ? "edit" : data.name === MSG_CMD_DEL ? "del" : null; + data.name === MSG_CMD_ADD + ? "add" + : data.name === MSG_CMD_EDIT + ? "edit" + : data.name === MSG_CMD_DEL + ? "del" + : null; if (!op) return; const target = data.target_id ? data.resolved?.messages?.[data.target_id] : undefined; const source = target?.embeds?.[0]?.url ?? target?.content ?? ""; @@ -403,7 +447,14 @@ export class DiscordGateway { if (top?.name === "logout") return this.cmdLogout(id, token, userId); if (top?.name === "comment") { const sub = top.options?.[0]; - const op = sub?.name === "add" ? "add" : sub?.name === "edit" ? "edit" : sub?.name === "del" ? "del" : null; + const op = + sub?.name === "add" + ? "add" + : sub?.name === "edit" + ? "edit" + : sub?.name === "del" + ? "del" + : null; if (!op) return; const link = sub?.options?.find((o) => o.name === "link")?.value ?? ""; return this.commentOp(id, token, userId, op, link); @@ -518,7 +569,8 @@ export class DiscordGateway { private async cmdLogin(id: string, token: string, userId: string | null): Promise { if (!userId) return this.respond(id, token, "无法识别你的 Discord 账号。"); const clientId = this.env.GITHUB_CLIENT_ID; - if (!clientId) return this.respond(id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。"); + if (!clientId) + return this.respond(id, token, "服务器未配置 GitHub OAuth(GITHUB_CLIENT_ID)。"); const state = crypto.randomUUID().replace(/-/g, ""); await this.env.KV.put( @@ -543,7 +595,8 @@ export class DiscordGateway { /** Map a GitHub op error code to a user-facing (Chinese) message. */ private 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}`; @@ -568,14 +621,28 @@ export class DiscordGateway { if (op === "add") { const m = source.match(GITHUB_ISSUE_RE); - if (!m) return this.respond(id, token, "找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。"); - return this.openCommentModal(id, token, `${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`, `评论 ${m[1]}/${m[2]}#${m[3]}`); + if (!m) + return this.respond( + id, + token, + "找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。", + ); + return this.openCommentModal( + id, + token, + `${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`, + `评论 ${m[1]}/${m[2]}#${m[3]}`, + ); } // edit / del both need a specific comment id. const m = source.match(GITHUB_COMMENT_RE); if (!m) { - return this.respond(id, token, "找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。"); + return this.respond( + id, + token, + "找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。", + ); } const [, owner, repo, commentId] = m; @@ -591,7 +658,13 @@ export class DiscordGateway { // edit: fetch current body to prefill the modal. let prefill = ""; try { - const { body } = await getCommentAsUser(this.env.KV, githubUserId, owner!, repo!, Number(commentId)); + const { body } = await getCommentAsUser( + this.env.KV, + githubUserId, + owner!, + repo!, + Number(commentId), + ); prefill = body; } catch (err) { return this.respond(id, token, this.errText(err)); @@ -662,7 +735,8 @@ export class DiscordGateway { // ghc|add|owner|repo|issueNumber if (customId.startsWith(MODAL_ADD)) { const [owner, repo, number] = customId.slice(MODAL_ADD.length).split("|"); - if (!owner || !repo || !number) return this.respond(id, token, "内部错误:无法解析目标 issue。"); + if (!owner || !repo || !number) + return this.respond(id, token, "内部错误:无法解析目标 issue。"); try { const { htmlUrl, login } = await commentAsUser( this.env.KV, @@ -681,7 +755,8 @@ export class DiscordGateway { // ghc|edit|owner|repo|commentId if (customId.startsWith(MODAL_EDIT)) { const [owner, repo, commentId] = customId.slice(MODAL_EDIT.length).split("|"); - if (!owner || !repo || !commentId) return this.respond(id, token, "内部错误:无法解析目标评论。"); + if (!owner || !repo || !commentId) + return this.respond(id, token, "内部错误:无法解析目标评论。"); try { const { htmlUrl } = await editCommentAsUser( this.env.KV, diff --git a/src/formatter.ts b/src/formatter.ts index ee3f47f..0590d22 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -67,7 +67,11 @@ const WORKFLOW_CONCLUSION_EMOJI: Record = { type T = (key: string, params?: Record) => string; -export function formatEvent(route: Route, event: WebhookEvent, tr?: Translations): FormattedMessage { +export function formatEvent( + route: Route, + event: WebhookEvent, + tr?: Translations, +): FormattedMessage { const { event: eventType, payload } = event; const repo = (payload.repository as { full_name?: string })?.full_name; const sender = (payload.sender as { login?: string })?.login; @@ -163,7 +167,9 @@ function formatPush( descriptionParts.push(t("events.push.branch_created")); } - descriptionParts.push(t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" })); + descriptionParts.push( + t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" }), + ); if (compareUrl) { descriptionParts.push(t("events.push.view_comparison", { url: compareUrl })); @@ -219,7 +225,11 @@ function formatPush( embeds: [ { author, - title: t("events.push.title", { count, s: count !== 1 ? "s" : "", repo: repo ?? t("common.repository") }), + title: t("events.push.title", { + count, + s: count !== 1 ? "s" : "", + repo: repo ?? t("common.repository"), + }), url: compareUrl, color: GITHUB_COLORS.push, description: descriptionParts.join("\n"), @@ -331,7 +341,11 @@ function formatPullRequest( embeds: [ { author, - title: t("events.pr.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }), + title: t("events.pr.title", { + repo: repo ?? t("common.repository"), + number: pr.number ?? "?", + title: pr.title ?? t("common.untitled"), + }), url: pr.html_url, color: GITHUB_COLORS[colorKey], description: descriptionParts.join("\n"), @@ -412,7 +426,11 @@ function formatIssues( embeds: [ { author, - title: t("events.issues.title", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }), + title: t("events.issues.title", { + repo: repo ?? t("common.repository"), + number: issue.number ?? "?", + title: issue.title ?? t("common.untitled"), + }), url: issue.html_url, color: GITHUB_COLORS[colorKey], description: descriptionParts.join("\n"), @@ -449,7 +467,11 @@ function formatIssueComment( embeds: [ { author, - title: t("events.issue_comment.comment_on", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }), + title: t("events.issue_comment.comment_on", { + repo: repo ?? t("common.repository"), + number: issue.number ?? "?", + title: issue.title ?? t("common.untitled"), + }), url: comment.html_url ?? issue.html_url, color: GITHUB_COLORS.issue_comment, description: `${t("events.issue_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`, @@ -563,7 +585,13 @@ function formatRelease( const emoji = action === "deleted" ? "🗑️" : isPrerelease ? "⚠️" : "🚀"; const descriptionParts: string[] = []; - descriptionParts.push(t("events.release.action_release", { emoji, action: al, tag: release.tag_name ?? t("common.unknown") })); + descriptionParts.push( + t("events.release.action_release", { + emoji, + action: al, + tag: release.tag_name ?? t("common.unknown"), + }), + ); if (release.body) { const truncated = release.body.slice(0, 300); @@ -574,7 +602,10 @@ function formatRelease( embeds: [ { author, - title: t("events.release.title", { name: release.name ?? release.tag_name ?? "Release", repo: repo ?? t("common.repository") }), + title: t("events.release.title", { + name: release.name ?? release.tag_name ?? "Release", + repo: repo ?? t("common.repository"), + }), url: release.html_url, color: GITHUB_COLORS[colorKey], description: descriptionParts.join("\n"), @@ -622,7 +653,12 @@ function formatCreate( embeds: [ { author, - title: t("events.create.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }), + title: t("events.create.title", { + emoji, + type: refType, + ref, + repo: repo ?? t("common.repository"), + }), color: GITHUB_COLORS.create, fields, footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, @@ -647,7 +683,12 @@ function formatDelete( embeds: [ { author, - title: t("events.delete.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }), + title: t("events.delete.title", { + emoji, + type: refType, + ref, + repo: repo ?? t("common.repository"), + }), color: GITHUB_COLORS.delete, footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, timestamp: new Date().toISOString(), @@ -693,7 +734,10 @@ function formatFork( embeds: [ { author, - title: t("events.fork.title", { repo: repo ?? t("common.repository"), forkee: forkee?.full_name ?? t("common.unknown") }), + title: t("events.fork.title", { + repo: repo ?? t("common.repository"), + forkee: forkee?.full_name ?? t("common.unknown"), + }), url: forkee?.html_url ?? repoUrl, color: GITHUB_COLORS.fork, footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, @@ -797,7 +841,11 @@ function formatPullRequestReview( embeds: [ { author, - title: t("events.pr_review.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }), + title: t("events.pr_review.title", { + repo: repo ?? t("common.repository"), + number: pr.number ?? "?", + title: pr.title ?? t("common.untitled"), + }), url: review.html_url, color: GITHUB_COLORS[colorKey], description: descriptionParts.join("\n"), @@ -833,7 +881,10 @@ function formatPullRequestReviewComment( const fields: Array<{ name: string; value: string; inline?: boolean }> = []; if (comment.path) { - const loc = comment.position != null ? t("events.pr_review_comment.line", { position: comment.position }) : ""; + const loc = + comment.position != null + ? t("events.pr_review_comment.line", { position: comment.position }) + : ""; fields.push({ name: t("fields.file"), value: `\`${comment.path}\`${loc}`, @@ -845,7 +896,10 @@ function formatPullRequestReviewComment( embeds: [ { author, - title: t("events.pr_review_comment.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?" }), + title: t("events.pr_review_comment.title", { + repo: repo ?? t("common.repository"), + number: pr.number ?? "?", + }), url: comment.html_url, color: GITHUB_COLORS.pull_request_review_commented, description: `${t("events.pr_review_comment.action_inline", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`, @@ -889,7 +943,10 @@ function formatCommitComment( embeds: [ { author, - title: t("events.commit_comment.title", { sha: shortSha, repo: repo ?? t("common.repository") }), + title: t("events.commit_comment.title", { + sha: shortSha, + repo: repo ?? t("common.repository"), + }), url: comment.html_url, color: GITHUB_COLORS.commit_comment, description: `${t("events.commit_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`, @@ -1062,7 +1119,11 @@ function formatLabel( embeds: [ { author, - title: t("events.label.title", { emoji, action: al, name: label.name ?? t("common.unknown") }), + title: t("events.label.title", { + emoji, + action: al, + name: label.name ?? t("common.unknown"), + }), color: GITHUB_COLORS.label, fields: fields.length > 0 ? fields : undefined, footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, @@ -1133,7 +1194,11 @@ function formatMilestone( embeds: [ { author, - title: t("events.milestone.title", { emoji: stateEmoji, action: al, title: milestone.title ?? t("common.unknown") }), + title: t("events.milestone.title", { + emoji: stateEmoji, + action: al, + title: milestone.title ?? t("common.unknown"), + }), url: milestone.html_url, color: milestone.state === "closed" @@ -1179,13 +1244,20 @@ function formatDiscussion( embeds: [ { author, - title: t("events.discussion.title", { emoji: stateEmoji, number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }), + title: t("events.discussion.title", { + emoji: stateEmoji, + number: discussion.number ?? "?", + title: discussion.title ?? t("common.untitled"), + }), url: discussion.html_url, color: action === "answered" ? GITHUB_COLORS.discussion_answered : GITHUB_COLORS.discussion_created, - description: t("events.discussion.action_discussion", { action: al, category: category ? ` in **${category}**` : "" }), + description: t("events.discussion.action_discussion", { + action: al, + category: category ? ` in **${category}**` : "", + }), footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, timestamp: new Date().toISOString(), }, @@ -1217,7 +1289,10 @@ function formatDiscussionComment( embeds: [ { author, - title: t("events.discussion_comment.comment_on", { number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }), + title: t("events.discussion_comment.comment_on", { + number: discussion.number ?? "?", + title: discussion.title ?? t("common.untitled"), + }), url: comment.html_url, color: GITHUB_COLORS.discussion_comment, description: `${t("events.discussion_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`, @@ -1243,7 +1318,8 @@ function formatRepository( if (action === "renamed") { const changes = payload.changes as { name?: { from?: string } } | undefined; - const newName = (payload.repository as { full_name?: string })?.full_name ?? t("common.unknown"); + const newName = + (payload.repository as { full_name?: string })?.full_name ?? t("common.unknown"); fields.push({ name: t("fields.renamed"), value: changes?.name?.from ? `${changes.name.from} → ${newName}` : newName, @@ -1492,7 +1568,10 @@ function formatGeneric( embeds: [ { author, - title: t("events.generic.title", { event: eventType, action: payload.action ? `: ${payload.action}` : "" }), + title: t("events.generic.title", { + event: eventType, + action: payload.action ? `: ${payload.action}` : "", + }), color: GITHUB_COLORS.default, footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) }, timestamp: new Date().toISOString(), diff --git a/src/i18n.ts b/src/i18n.ts index 14c3c0f..9146342 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -53,6 +53,9 @@ export function t( translations?: Translations, ): string { const dict = translations ?? en; - const raw = getByPath(dict as Record, key) ?? getByPath(en as Record, key) ?? key; + const raw = + getByPath(dict as Record, key) ?? + getByPath(en as Record, key) ?? + key; return params ? interpolate(raw, params) : raw; } diff --git a/src/send-log.ts b/src/send-log.ts index 4749f11..2241172 100644 --- a/src/send-log.ts +++ b/src/send-log.ts @@ -35,9 +35,7 @@ export async function recordSend(kv: KVNamespace, record: SendRecord): Promise { try { const list = await kv.list({ prefix: KEY_PREFIX, limit: MAX_READ }); - const records = await Promise.all( - list.keys.map((k) => kv.get(k.name, "json")), - ); + const records = await Promise.all(list.keys.map((k) => kv.get(k.name, "json"))); return records .filter((r): r is SendRecord => r != null) .sort((a, b) => b.ts - a.ts) diff --git a/src/token-store.ts b/src/token-store.ts index 118f846..c564836 100644 --- a/src/token-store.ts +++ b/src/token-store.ts @@ -87,9 +87,6 @@ export async function getDiscordLink( return await kv.get(`discord-link:${discordUserId}`, "text"); } -export async function removeDiscordLink( - kv: KVNamespace, - discordUserId: string, -): Promise { +export async function removeDiscordLink(kv: KVNamespace, discordUserId: string): Promise { await kv.delete(`discord-link:${discordUserId}`); }