aboutsummaryrefslogtreecommitdiff
path: root/src/scraper.ts
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-01-27 15:05:36 -0500
committerJoe Mou <dev@mou.fo>2026-01-27 17:21:45 -0500
commit200defaa5c769b4603f9256e943821344cadbe82 (patch)
treec66bc43948d8accf8cd2d5462c6115c8cc238ca7 /src/scraper.ts
parent8fec0b99fbe193326d25fc107e0c625dcf05fd12 (diff)
List issues
Diffstat (limited to 'src/scraper.ts')
-rw-r--r--src/scraper.ts85
1 files changed, 82 insertions, 3 deletions
diff --git a/src/scraper.ts b/src/scraper.ts
index b76f8c8..bbf7511 100644
--- a/src/scraper.ts
+++ b/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,
+ };
+}