diff --git a/ts/send.ts b/ts/send.ts index f2424b0a..a74358ba 100644 --- a/ts/send.ts +++ b/ts/send.ts @@ -1,24 +1,25 @@ import { Bot, InlineKeyboard } from "grammy"; import { z } from "zod"; -const EnvSchema = z.object({ - BOT_TOKEN: z.string(), - TELEGRAM_GROUP_ID: z.string(), - TELEGRAM_TOPIC_COMMITS: z.string(), - TELEGRAM_TOPIC_PRS: z.string(), - GITHUB_TOKEN: z.string(), - GITHUB_EVENT_PATH: z.string(), -}); -const env = EnvSchema.parse(process.env); +const BOT_TOKEN = process.env.BOT_TOKEN!; +const GROUP_ID = Number(process.env.TELEGRAM_GROUP_ID!); +const TOPIC_COMMITS = Number(process.env.TELEGRAM_TOPIC_COMMITS!); +const TOPIC_PRS = Number(process.env.TELEGRAM_TOPIC_PRS!); +const GITHUB_TOKEN = process.env.GITHUB_TOKEN!; +const EVENT_PATH = process.env.GITHUB_EVENT_PATH!; -const bot = new Bot(env.BOT_TOKEN); +const bot = new Bot(BOT_TOKEN); + +const FileSchema = z.object({ + path: z.string(), +}); const PullRequestSchema = z.object({ action: z.string(), number: z.number(), repository: z.object({ full_name: z.string(), - html_url: z.url(), + html_url: z.url() }), pull_request: z.object({ url: z.url(), @@ -26,74 +27,55 @@ const PullRequestSchema = z.object({ body: z.string().nullable(), user: z.object({ login: z.string(), - html_url: z.url(), + html_url: z.url() }), head: z.object({ ref: z.string() }), base: z.object({ ref: z.string() }), changed_files: z.number(), additions: z.number(), - deletions: z.number(), - }), + deletions: z.number() + }) }); const PushSchema = z.object({ ref: z.string(), repository: z.object({ full_name: z.string(), - html_url: z.url(), + html_url: z.url() }), - head_commit: z - .object({ - id: z.string(), - url: z.url(), - message: z.string(), - author: z.object({ - name: z.string(), - email: z.email(), - }), - added: z.array(z.string()), - modified: z.array(z.string()), - removed: z.array(z.string()), - }) - .nullable(), + head_commit: z.object({ + id: z.string(), + url: z.url(), + message: z.string(), + author: z.object({ + name: z.string(), + email: z.email() + }), + added: z.array(z.string()), + modified: z.array(z.string()), + removed: z.array(z.string()) + }).nullable() }); function detectLanguage(files: string[]): string { - const extCount = files.reduce>((count, file) => { - const ext = file.split(".").pop()?.toLowerCase() ?? ""; - count[ext] = (count[ext] || 0) + 1; - return count; - }, {}); - if (extCount.kt || extCount.kts) return "Kotlin"; - if (extCount.c && !extCount.h) return "C"; - if (extCount.rs) return "Rust"; - if (extCount.sh) return "Shell"; - if (extCount.ts || extCount.tsx) return "TypeScript"; + const ext = files.map(f => f.split(".").pop()?.toLowerCase() || ""); + if (ext.includes("kt") || ext.includes("kts")) return "Kotlin"; + if (ext.includes("c") && !ext.includes("h")) return "C"; + if (ext.includes("rs")) return "Rust"; + if (ext.includes("sh")) return "Shell"; + if (ext.includes("ts") || ext.includes("tsx")) return "TypeScript"; return "Unknown"; } -interface GitHubPRFilesResponse { - data: { - repository: { - pullRequest: { - files: { - nodes: { path: string }[]; - }; - }; - }; - }; -} - -async function fetchPrFiles( - repoFullName: string, - prNumber: number, -): Promise { +async function fetchPrFiles(repoFullName: string, prNumber: number): Promise { const query = ` query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { files(first: 100) { - nodes { path } + nodes { + path + } } } } @@ -102,25 +84,17 @@ async function fetchPrFiles( const resp = await fetch("https://api.github.com/graphql", { method: "POST", headers: { - Authorization: `bearer ${env.GITHUB_TOKEN}`, - "Content-Type": "application/json", + "Authorization": `bearer ${GITHUB_TOKEN}`, + "Content-Type": "application/json" }, - body: JSON.stringify({ - query, - variables: { owner, name, number: prNumber }, - }), + body: JSON.stringify({ query, variables: { owner, name, number: prNumber } }) }); - if (!resp.ok) { - throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`); - } - const json = (await resp.json()) as GitHubPRFilesResponse; - if (!json?.data?.repository?.pullRequest?.files?.nodes) return []; - return json.data.repository.pullRequest.files.nodes.map((n) => n.path); + const json = await resp.json() as any; + const nodes = json.data.repository.pullRequest.files.nodes as { path: string }[]; + return nodes.map(n => n.path); } -async function formatPrMessage( - evt: z.infer, -): Promise { +async function formatPrMessage(evt: z.infer): Promise { const files = await fetchPrFiles(evt.repository.full_name, evt.number); const lang = detectLanguage(files); return `Repository: [${evt.repository.full_name}](${evt.repository.html_url}) @@ -129,7 +103,7 @@ Author: [${evt.pull_request.user.login}](${evt.pull_request.user.html_url}) Files Changed: ${evt.pull_request.changed_files}, +${evt.pull_request.additions}/- ${evt.pull_request.deletions} Likely Language: ${lang} Title: ${evt.pull_request.title} -Description: ${evt.pull_request.body ?? "_None provided_"}`; +Description: ${evt.pull_request.body ?? "_None provided_"}` } function formatPushMessage(evt: z.infer): string { @@ -141,10 +115,8 @@ Push event but no head commit data.`; const details = [ c.added.length ? `Added: ${c.added.join(", ")}` : "", c.modified.length ? `Modified: ${c.modified.join(", ")}` : "", - c.removed.length ? `Removed: ${c.removed.join(", ")}` : "", - ] - .filter(Boolean) - .join("\n"); + c.removed.length ? `Removed: ${c.removed.join(", ")}` : "" + ].filter(Boolean).join("\n"); const lang = detectLanguage([...c.added, ...c.modified, ...c.removed]); return `Repository: [${evt.repository.full_name}](${evt.repository.html_url}) Commit: [${c.id}](${c.url}) @@ -154,46 +126,32 @@ ${details ? details + "\n" : ""}Detected Language: ${lang}`; } async function main(): Promise { + const raw = await (await import("node:fs/promises")).readFile(EVENT_PATH, "utf-8"); + const parsed = JSON.parse(raw); + let message: string; + let topic: number; + try { - const raw = await Bun.file(env.GITHUB_EVENT_PATH).text(); - const parsed = JSON.parse(raw); - - let message: string; - let topic: number; - - const prResult = PullRequestSchema.safeParse(parsed); - if (prResult.success) { - message = await formatPrMessage(prResult.data); - topic = Number(env.TELEGRAM_TOPIC_PRS); - } else { - const pushResult = PushSchema.safeParse(parsed); - if (!pushResult.success) { - console.error( - "Unrecognized event payload", - prResult.error, - pushResult.error, - ); - process.exit(1); - } - message = formatPushMessage(pushResult.data); - topic = Number(env.TELEGRAM_TOPIC_COMMITS); - } - - const keyboard = new InlineKeyboard().url( - "View on GitHub", - (parsed.repository as any).html_url as string, - ); - await bot.api.sendMessage(Number(env.TELEGRAM_GROUP_ID), message, { - parse_mode: "Markdown", - message_thread_id: topic, - reply_markup: keyboard, - }); - - process.exit(0); - } catch (err) { - console.error("Fatal error in main execution:", err); - process.exit(1); + const prEvt = PullRequestSchema.parse(parsed); + message = await formatPrMessage(prEvt); + topic = TOPIC_PRS; + } catch { + const pushEvt = PushSchema.parse(parsed); + message = formatPushMessage(pushEvt); + topic = TOPIC_COMMITS; } + + const keyboard = new InlineKeyboard().url("View on GitHub", (parsed.repository as any).html_url as string); + await bot.api.sendMessage(GROUP_ID, message, { + parse_mode: "Markdown", + message_thread_id: topic, + reply_markup: keyboard + }); + + process.exit(0); } -void main(); +main().catch(err => { + console.error(err); + process.exit(1); +});