diff options
| author | Joe Mou <dev@mou.fo> | 2026-01-27 15:05:36 -0500 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-01-27 17:21:45 -0500 |
| commit | d29bdad2f5c8326d111901373f0a56b3a09a2cd8 (patch) | |
| tree | a34128312316849e257bde7750c2e01180a77f3c /cgithub/src/scraper.ts | |
| parent | 64c397e256b01a2e542e84529e947e237c07edd6 (diff) | |
List issues
Diffstat (limited to 'cgithub/src/scraper.ts')
| -rw-r--r-- | cgithub/src/scraper.ts | 85 |
1 files changed, 82 insertions, 3 deletions
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<string, string>; } @@ -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<string> { // 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<string, string> | unde return Object.keys(result).length > 0 ? result : undefined; } -function extractGitHub<T>(payload: RawPayload, extra: T): GitHubCommon & T { +function extractGitHub<T>(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<GitHubIssues> { + 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, + }; +} |
