mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +00:00
feat(api): add SRS Cloudflare Worker with email digest, charts, and D1 integration
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* GitHub REST + GraphQL client on GH_PAT. Hand-rolled fetch, no SDK —
|
||||
* matching the repo's dependency-free automation rule.
|
||||
*/
|
||||
|
||||
export interface GitHub {
|
||||
repo: string;
|
||||
rest(path: string, init?: RequestInit): Promise<unknown>;
|
||||
graphql(query: string, variables?: Record<string, unknown>): Promise<unknown>;
|
||||
list(path: string): AsyncGenerator<unknown, void, void>;
|
||||
/** Comment first, then close — a failed close still leaves a visible note. */
|
||||
closeIssue(issue: number, comment: string): Promise<void>;
|
||||
comment(issue: number, body: string): Promise<void>;
|
||||
react(commentId: number, content: string): Promise<void>;
|
||||
}
|
||||
|
||||
const PER_PAGE = 100;
|
||||
|
||||
export function github(token: string, repo: string): GitHub {
|
||||
async function rest(path: string, init: RequestInit = {}): Promise<unknown> {
|
||||
const res = await fetch(`https://api.github.com${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
accept: "application/vnd.github+json",
|
||||
authorization: `Bearer ${token}`,
|
||||
"user-agent": "srs-api",
|
||||
"x-github-api-version": "2022-11-28",
|
||||
...(init.body ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`${init.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`);
|
||||
}
|
||||
return res.status === 204 ? null : res.json();
|
||||
}
|
||||
|
||||
async function graphql(
|
||||
query: string,
|
||||
variables: Record<string, unknown> = {},
|
||||
): Promise<unknown> {
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
"user-agent": "srs-api",
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`graphql -> ${res.status} ${await res.text()}`);
|
||||
const payload = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
|
||||
if (payload.errors?.length) throw new Error(payload.errors.map((e) => e.message).join("; "));
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
async function* list(path: string): AsyncGenerator<unknown, void, void> {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
for (let page = 1; ; page++) {
|
||||
const batch = await rest(`${path}${sep}per_page=${PER_PAGE}&page=${page}`);
|
||||
if (!Array.isArray(batch)) throw new Error(`unexpected ${path} payload: not an array`);
|
||||
yield* batch;
|
||||
if (batch.length < PER_PAGE) return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repo,
|
||||
rest,
|
||||
graphql,
|
||||
list,
|
||||
async closeIssue(issue, comment) {
|
||||
await rest(`/repos/${repo}/issues/${issue}/comments`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ body: comment }),
|
||||
});
|
||||
await rest(`/repos/${repo}/issues/${issue}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ state: "closed", state_reason: "completed" }),
|
||||
});
|
||||
},
|
||||
async comment(issue, body) {
|
||||
await rest(`/repos/${repo}/issues/${issue}/comments`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ body }),
|
||||
});
|
||||
},
|
||||
async react(commentId, content) {
|
||||
await rest(`/repos/${repo}/issues/comments/${commentId}/reactions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user