Files
SukiSU-Ultra/ts/send.ts
2025-11-23 19:01:57 +05:30

198 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Bot, InlineKeyboard } from "grammy";
import { z } from "zod";
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(BOT_TOKEN);
const FileNode = 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(),
}),
pull_request: z.object({
html_url: z.url().optional(),
url: z.url().optional(),
title: z.string(),
body: z.string().nullable(),
user: z.object({ login: z.string(), html_url: z.url() }),
head: z.object({ ref: z.string() }),
base: z.object({ ref: z.string() }),
changed_files: z.number().optional().default(0),
additions: z.number().optional().default(0),
deletions: z.number().optional().default(0),
}),
});
const PushSchema = z.object({
ref: z.string(),
repository: z.object({
full_name: z.string(),
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()).optional().default([]),
modified: z.array(z.string()).optional().default([]),
removed: z.array(z.string()).optional().default([]),
})
.nullable(),
});
function detectLanguage(files: string[]): string {
const ext = files.map((f) => (f.split(".").pop() || "").toLowerCase());
if (ext.some((e) => e === "kt" || e === "kts")) return "Kotlin";
if (ext.some((e) => e === "rs")) return "Rust";
if (ext.some((e) => e === "c")) return "C";
if (ext.some((e) => e === "sh")) return "Shell";
if (ext.some((e) => e === "ts" || e === "tsx")) return "TypeScript";
return "Other";
}
async function fetchPrFiles(
repoFullName: string,
prNumber: number,
): Promise<string[]> {
const query = `
query($owner:String!, $name:String!, $number:Int!) {
repository(owner:$owner, name:$name) {
pullRequest(number:$number) {
files(first:100) {
nodes {
path
}
}
}
}
}`;
const [owner, name] = repoFullName.split("/");
const resp = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `bearer ${GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
variables: { owner, name, number: prNumber },
}),
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`GitHub GraphQL API error: ${resp.status} ${body}`);
}
const json = (await resp.json()) as any;
const nodes = json?.data?.repository?.pullRequest?.files?.nodes as
| { path: string }[]
| undefined;
if (!nodes || !Array.isArray(nodes)) return [];
return nodes.map((n) => n.path);
}
function prUrlOf(pr: { html_url?: string; url?: string }) {
return pr.html_url ?? pr.url ?? "";
}
async function formatPrMessage(
evt: z.infer<typeof PullRequestSchema>,
): Promise<{ text: string; fileLink: string }> {
const pr = evt.pull_request;
const repo = evt.repository;
const files = await fetchPrFiles(repo.full_name, evt.number);
const lang = detectLanguage(files);
const prUrl = prUrlOf(pr);
const fileLink = prUrl ? `${prUrl}/files` : repo.html_url;
const bodyText = pr.body ? pr.body : "_No description provided_";
const text =
`### Repository\n[${repo.full_name}](${repo.html_url})\n\n` +
`**Pull Request #${evt.number}:** [${pr.title}](${prUrl || repo.html_url})\n\n` +
`**Author:** [${pr.user.login}](${pr.user.html_url})\n` +
`**Files Changed:** ${pr.changed_files}\n` +
`**Additions / Deletions:** +${pr.additions} / -${pr.deletions}\n` +
`**Language:** ${lang}\n\n` +
`**Description:**\n\`\`\`\n${bodyText}\n\`\`\``;
return { text, fileLink };
}
function formatPushMessage(evt: z.infer<typeof PushSchema>): {
text: string;
fileLink: string;
} {
const repo = evt.repository;
const c = evt.head_commit;
if (!c) {
const text = `### Repository\n[${repo.full_name}](${repo.html_url})\n\nPush event detected, but no head commit data available.`;
return { text, fileLink: repo.html_url };
}
const added = c.added ?? [];
const modified = c.modified ?? [];
const removed = c.removed ?? [];
const details = [
added.length ? ` Added: ${added.join(", ")}` : "",
modified.length ? `✏️ Modified: ${modified.join(", ")}` : "",
removed.length ? `❌ Removed: ${removed.join(", ")}` : "",
]
.filter(Boolean)
.join("\n");
const lang = detectLanguage([...added, ...modified, ...removed]);
const fileLink = c.url;
const text =
`### Repository\n[${repo.full_name}](${repo.html_url})\n\n` +
`**Commit:** [${c.id}](${c.url})\n` +
`**Author:** ${c.author.name} <${c.author.email}>\n` +
`**Message:**\n\`\`\`\n${c.message}\n\`\`\`\n` +
(details ? `**Changes:**\n${details}\n` : "") +
`**Language:** ${lang}`;
return { text, fileLink };
}
async function main(): Promise<void> {
const raw = await (
await import("node:fs/promises")
).readFile(EVENT_PATH, "utf-8");
const parsed = JSON.parse(raw);
let messageObj: { text: string; fileLink: string };
let topic: number;
if ("pull_request" in parsed) {
const prEvt = PullRequestSchema.parse(parsed);
messageObj = await formatPrMessage(prEvt);
topic = TOPIC_PRS;
} else {
const pushEvt = PushSchema.parse(parsed);
messageObj = formatPushMessage(pushEvt);
topic = TOPIC_COMMITS;
}
const repoUrl =
(parsed.repository &&
(parsed.repository.html_url ?? parsed.repository.url)) ||
"";
const keyboard = new InlineKeyboard()
.url("View on GitHub", repoUrl)
.row()
.url("View Files", messageObj.fileLink);
await bot.api.sendMessage(GROUP_ID, messageObj.text, {
parse_mode: "Markdown",
message_thread_id: topic,
reply_markup: keyboard,
});
process.exit(0);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});