function requireEnv(key: string): string { const value = process.env[key]; if (!value) throw new Error(`Missing required env var: ${key}`); return value; } const config = { githubApiKey: requireEnv("GH_API_KEY"), forgejoApiKey: requireEnv("FJ_API_KEY"), forgejoUsername: requireEnv("FJ_USER"), forgejoURL: requireEnv("FJ_URL"), }; const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); const formattedOneYearAgo = oneYearAgo.toISOString().replace("Z", "+00:00"); const ghQuery = ` { user(login: "aselimov") { contributionsCollection { commitContributionsByRepository(maxRepositories: 100) { repository { nameWithOwner isMirror isPrivate } contributions(first: 100) { nodes { occurredAt commitCount } } } } } } `; type DayContributions = { [repo: string]: number; }; type ActivityMap = { [day: string]: DayContributions; }; function mergeInto(target: ActivityMap, source: ActivityMap): void { for (const [day, repos] of Object.entries(source)) { if (!target[day]) target[day] = {}; for (const [repo, count] of Object.entries(repos)) { target[day]![repo] = (target[day]![repo] ?? 0) + count; } } } function repoBaseName(fullName: string): string { return fullName.split("/")[1]?.toLowerCase() ?? fullName.toLowerCase(); } function deduplicateAcrossPlatforms( ghMap: ActivityMap, fjMap: ActivityMap, ): ActivityMap { const ghBaseNames = new Set(); for (const repos of Object.values(ghMap)) { for (const repo of Object.keys(repos)) { if (repo !== "private") ghBaseNames.add(repoBaseName(repo)); } } const sharedBaseNames = new Set(); for (const repos of Object.values(fjMap)) { for (const repo of Object.keys(repos)) { if (repo !== "private" && ghBaseNames.has(repoBaseName(repo))) { sharedBaseNames.add(repoBaseName(repo)); } } } const result: ActivityMap = {}; const allDays = new Set([...Object.keys(ghMap), ...Object.keys(fjMap)]); for (const day of allDays) { result[day] = {}; const ghDay = ghMap[day] ?? {}; const fjDay = fjMap[day] ?? {}; for (const [repo, count] of Object.entries(ghDay)) { const base = repoBaseName(repo); if (repo !== "private" && sharedBaseNames.has(base)) { const fjRepo = Object.keys(fjDay).find((r) => repoBaseName(r) === base); const fjCount = fjRepo ? (fjDay[fjRepo] ?? 0) : 0; if (count >= fjCount) { result[day]![repo] = count; } // else: Forgejo wins, added below } else { result[day]![repo] = (result[day]![repo] ?? 0) + count; } } for (const [repo, count] of Object.entries(fjDay)) { const base = repoBaseName(repo); if (repo !== "private" && sharedBaseNames.has(base)) { const ghRepo = Object.keys(ghDay).find((r) => repoBaseName(r) === base); const ghCount = ghRepo ? (ghDay[ghRepo] ?? 0) : 0; if (count > ghCount) { if (ghRepo) delete result[day]![ghRepo]; result[day]![repo] = count; } // else: GitHub already won } else { result[day]![repo] = (result[day]![repo] ?? 0) + count; } } if (Object.keys(result[day]!).length === 0) { delete result[day]; } } return result; } function parseGitHubContributions(data: any): ActivityMap { const result: ActivityMap = {}; const repos = data?.data?.user?.contributionsCollection ?.commitContributionsByRepository ?? []; for (const repoEntry of repos) { const repoName: string = repoEntry.repository.nameWithOwner; const isMirror: boolean = repoEntry.repository.isMirror; const isPrivate: boolean = repoEntry.repository.isPrivate; if (isMirror) continue; const displayName = isPrivate ? "private" : repoName; for (const contribution of repoEntry.contributions.nodes) { const day: string = contribution.occurredAt.split("T")[0]; const count: number = contribution.commitCount; if (!result[day]) result[day] = {}; result[day]![displayName] = (result[day]![displayName] ?? 0) + count; } } return result; } function parseForgejoActivity(activities: any[]): ActivityMap { const result: ActivityMap = {}; for (const activity of activities) { if (activity.repo?.mirror) continue; if (activity.op_type !== "commit_repo") continue; const isPrivate: boolean = activity.repo?.private ?? false; const repoFullName: string = activity.repo?.full_name ?? "unknown"; const displayName = isPrivate ? "private" : repoFullName; let content: { Commits?: { Timestamp: string | number }[] }; try { content = JSON.parse(activity.content); } catch { continue; } for (const commit of content.Commits ?? []) { let day: string; if (typeof commit.Timestamp === "number") { day = new Date(commit.Timestamp * 1000).toISOString().split("T")[0]!; } else { day = commit.Timestamp.split("T")[0]!; } if (!result[day]) result[day] = {}; result[day]![displayName] = (result[day]![displayName] ?? 0) + 1; } } return result; } async function getForgejoActivityMap(): Promise { const pageLimit = 50; let page = 1; const map: ActivityMap = {}; while (true) { const response = await fetch( `${config.forgejoURL}/api/v1/users/${config.forgejoUsername}/activities/feeds?limit=${pageLimit}&page=${page}&after=${formattedOneYearAgo}`, { headers: { Authorization: `Bearer ${config.forgejoApiKey}` } }, ); if (!response.ok) { throw new Error( `Forgejo API error: ${response.status} ${response.statusText}`, ); } const activities: any[] = await response.json(); if (activities.length === 0) break; const pageMap = parseForgejoActivity(activities); mergeInto(map, pageMap); if (activities.length < pageLimit) break; page++; } return map; } async function makeActivityMap(): Promise { const ghResponse = await fetch("https://api.github.com/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.githubApiKey}`, }, body: JSON.stringify({ query: ghQuery }), }); if (!ghResponse.ok) { throw new Error( `GitHub API error: ${ghResponse.status} ${ghResponse.statusText}`, ); } const ghData = await ghResponse.json(); const ghMap = parseGitHubContributions(ghData); const fjMap = await getForgejoActivityMap(); return deduplicateAcrossPlatforms(ghMap, fjMap); } const activityMap = await makeActivityMap(); await Bun.write( "../static/activity.json", JSON.stringify(activityMap, null, 2), ); console.log( `Wrote ${Object.keys(activityMap).length} days of activity to out/activity.json`, );