From d29bdad2f5c8326d111901373f0a56b3a09a2cd8 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 27 Jan 2026 15:05:36 -0500 Subject: List issues --- cgithub/src/scraper.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) (limited to 'cgithub/src/scraper.ts') diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index b76f8c8..bbf7511 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -21,6 +21,38 @@ interface OverviewFile { loaded: boolean; } +interface IssueNode { + number: number; + title: string; + author?: { + login: string; + }; + createdAt: string; + state: string; + labels?: { + edges: { + node: { + name: string; + }; + }[]; + }; +} + +interface IssueIndexPageQuery { + queryName: "IssueIndexPageQuery"; + result: { + data: { + repository: { + search: { + edges: { + node: IssueNode; + }[]; + }; + }; + }; + }; +} + interface RawPayload { repo: { ownerLogin: string; @@ -42,6 +74,8 @@ interface RawPayload { overview?: { overviewFiles?: OverviewFile[]; }; + // Actually there may be other query types. + preloadedQueries?: IssueIndexPageQuery[]; } interface GitHubCommon { @@ -49,11 +83,14 @@ interface GitHubCommon { owner: string; name: string; }; +} + +interface GitHubNav extends GitHubCommon { branch: string; path: string; } -export interface GitHubTree extends GitHubCommon { +export interface GitHubTree extends GitHubNav { items: TreeItem[]; overviewHtml?: Record; } @@ -71,13 +108,26 @@ export interface GitHubRepo extends GitHubTree { info: RepoInfo; } -export interface GitHubBlob extends GitHubCommon { +export interface GitHubBlob extends GitHubNav { language: string | null; size: number | null; content: string; rawContent: string; } +interface Issue { + number: number; + title: string; + state: string; + createdAt: string; + numReplies?: number; + numPRs?: number; +} + +export interface GitHubIssues extends GitHubCommon { + issues: Issue[]; +} + async function fetchGitHubPage(path: string): Promise { // GitHub throttles/blocks requests without realistic browser headers. // These headers make the request appear as a standard browser visit. @@ -150,7 +200,7 @@ function extractOverviewHtml(payload: RawPayload): Record | unde return Object.keys(result).length > 0 ? result : undefined; } -function extractGitHub(payload: RawPayload, extra: T): GitHubCommon & T { +function extractGitHub(payload: RawPayload, extra: T): GitHubNav & T { return { repo: { owner: payload.repo.ownerLogin, @@ -231,3 +281,32 @@ export async function getGitHubBlob( size, }); } + +export async function getGitHubIssues(owner: string, repo: string): Promise { + const html = await fetchGitHubPage(`${owner}/${repo}/issues`); + + const payload = parsePayload(html, "react-app.embeddedData", ["payload"]); + + let issuesQuery: IssueIndexPageQuery | undefined; + for (const query of payload?.preloadedQueries ?? []) { + if (query.queryName === "IssueIndexPageQuery") { + issuesQuery = query; + } + } + if (issuesQuery === undefined) { + throw new Error("Could not find IssueIndexPageQuery in embedded JSON"); + } + + const issues: Issue[] = issuesQuery.result.data.repository.search.edges.map((edge) => { + const { number, title, state, createdAt } = edge.node; + return { number, title, state, createdAt }; + }); + + return { + repo: { + owner, + name: repo, + }, + issues, + }; +} -- cgit v1.3.1