import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../utils"; import { env } from "~/env/server"; import { withCacheAndStale } from "~/server/cache"; import { CACHE_CONFIG, NETWORK_CONFIG } from "~/config"; import { fetchWithTimeout, checkResponse, NetworkError, TimeoutError, APIError } from "~/server/fetch-utils"; interface GitCommit { sha: string; message: string; author: string; date: string; repo: string; url: string; } interface ContributionDay { date: string; count: number; } export const gitActivityRouter = createTRPCRouter({ getGitHubCommits: publicProcedure .input(z.object({ limit: z.number().default(3) })) .query(async ({ input }) => { return withCacheAndStale( `github-commits-${input.limit}`, CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { // Use Events API to get recent push events - much more efficient const eventsResponse = await fetchWithTimeout( `https://api.github.com/users/MikeFreno/events/public?per_page=100`, { headers: { Authorization: `Bearer ${env.GITHUB_API_TOKEN}`, Accept: "application/vnd.github.v3+json" }, timeout: NETWORK_CONFIG.GITHUB_API_TIMEOUT_MS } ); await checkResponse(eventsResponse); const events = await eventsResponse.json(); const allCommits: GitCommit[] = []; // Extract push events and fetch commit details for (const event of events) { if (event.type !== "PushEvent") continue; if (allCommits.length >= input.limit * 5) break; // Get extra to ensure we have enough const repoName = event.repo.name; const commitSha = event.payload.head; try { // Fetch the actual commit details to get the message const commitResponse = await fetchWithTimeout( `https://api.github.com/repos/${repoName}/commits/${commitSha}`, { headers: { Authorization: `Bearer ${env.GITHUB_API_TOKEN}`, Accept: "application/vnd.github.v3+json" }, timeout: 5000 } ); if (commitResponse.ok) { const commit = await commitResponse.json(); // Filter for your commits if ( commit.author?.login === "MikeFreno" || commit.author?.login === "mikefreno" || commit.commit?.author?.email?.includes("mike") ) { allCommits.push({ sha: commit.sha?.substring(0, 7) || "unknown", message: commit.commit?.message?.split("\n")[0] || "No message", author: commit.commit?.author?.name || commit.author?.login || "Unknown", date: commit.commit?.author?.date || new Date().toISOString(), repo: repoName, url: `https://github.com/${repoName}/commit/${commit.sha}` }); } } } catch (error) { if ( error instanceof NetworkError || error instanceof TimeoutError ) { console.warn( `Network error fetching commit ${commitSha} for ${repoName}, skipping` ); } else { console.error( `Error fetching commit ${commitSha} for ${repoName}:`, error ); } } } // Already sorted by event date, but sort again by commit date to be precise allCommits.sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); return allCommits.slice(0, input.limit); }, { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("GitHub API unavailable (network error)"); } else if (error instanceof TimeoutError) { console.error(`GitHub API timeout after ${error.timeoutMs}ms`); } else if (error instanceof APIError) { console.error( `GitHub API error: ${error.status} ${error.statusText}` ); } else { console.error("Unexpected error fetching GitHub commits:", error); } return []; }); }), getGiteaCommits: publicProcedure .input(z.object({ limit: z.number().default(3) })) .query(async ({ input }) => { return withCacheAndStale( `gitea-commits-${input.limit}`, CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { const reposResponse = await fetchWithTimeout( `${env.GITEA_URL}/api/v1/users/Mike/repos?limit=100`, { headers: { Authorization: `token ${env.GITEA_TOKEN}`, Accept: "application/json" }, timeout: NETWORK_CONFIG.GITHUB_API_TIMEOUT_MS } ); await checkResponse(reposResponse); const repos = await reposResponse.json(); const allCommits: GitCommit[] = []; for (const repo of repos) { if (allCommits.length >= input.limit * 3) break; // Get extra to sort later try { const commitsResponse = await fetchWithTimeout( `${env.GITEA_URL}/api/v1/repos/Mike/${repo.name}/commits?limit=5`, { headers: { Authorization: `token ${env.GITEA_TOKEN}`, Accept: "application/json" }, timeout: 10000 } ); if (commitsResponse.ok) { const commits = await commitsResponse.json(); for (const commit of commits) { if ( (commit.commit?.author?.email && commit.commit.author.email.includes( "michael@freno.me" )) || commit.commit.author.email.includes( "michaelt.freno@gmail.com" ) // Filter for your commits ) { allCommits.push({ sha: commit.sha?.substring(0, 7) || "unknown", message: commit.commit?.message?.split("\n")[0] || "No message", author: commit.commit?.author?.name || repo.owner.login, date: commit.commit?.author?.date || new Date().toISOString(), repo: repo.full_name, url: `${env.GITEA_URL}/${repo.full_name}/commit/${commit.sha}` }); } } } } catch (error) { if ( error instanceof NetworkError || error instanceof TimeoutError ) { console.warn( `Network error fetching commits for ${repo.name}, skipping` ); } else { console.error( `Error fetching commits for ${repo.name}:`, error ); } } } allCommits.sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); return allCommits.slice(0, input.limit); }, { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("Gitea API unavailable (network error)"); } else if (error instanceof TimeoutError) { console.error(`Gitea API timeout after ${error.timeoutMs}ms`); } else if (error instanceof APIError) { console.error(`Gitea API error: ${error.status} ${error.statusText}`); } else { console.error("Unexpected error fetching Gitea commits:", error); } return []; }); }), getGitHubActivity: publicProcedure.query(async () => { return withCacheAndStale( "github-activity", CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { const query = ` query($userName: String!) { user(login: $userName) { contributionsCollection { contributionCalendar { weeks { contributionDays { date contributionCount } } } } } } `; const response = await fetchWithTimeout( "https://api.github.com/graphql", { method: "POST", headers: { Authorization: `Bearer ${env.GITHUB_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ query, variables: { userName: "MikeFreno" } }), timeout: 15000 } ); await checkResponse(response); const data = await response.json(); if (data.errors) { console.error("GitHub GraphQL errors:", data.errors); throw new APIError("GraphQL query failed", 500, "GraphQL Error"); } const contributions: ContributionDay[] = []; const weeks = data.data?.user?.contributionsCollection?.contributionCalendar ?.weeks || []; for (const week of weeks) { for (const day of week.contributionDays) { contributions.push({ date: day.date, count: day.contributionCount }); } } return contributions; }, { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("GitHub GraphQL API unavailable (network error)"); } else if (error instanceof TimeoutError) { console.error(`GitHub GraphQL API timeout after ${error.timeoutMs}ms`); } else if (error instanceof APIError) { console.error( `GitHub GraphQL API error: ${error.status} ${error.statusText}` ); } else { console.error("Unexpected error fetching GitHub activity:", error); } return []; }); }), getGiteaActivity: publicProcedure.query(async () => { return withCacheAndStale( "gitea-activity", CACHE_CONFIG.GIT_ACTIVITY_CACHE_TTL_MS, async () => { const reposResponse = await fetchWithTimeout( `${env.GITEA_URL}/api/v1/user/repos?limit=100`, { headers: { Authorization: `token ${env.GITEA_TOKEN}`, Accept: "application/json" }, timeout: 15000 } ); await checkResponse(reposResponse); const repos = await reposResponse.json(); const contributionsByDay = new Map(); const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); for (const repo of repos) { try { const commitsResponse = await fetchWithTimeout( `${env.GITEA_URL}/api/v1/repos/${repo.owner.login}/${repo.name}/commits?limit=100`, { headers: { Authorization: `token ${env.GITEA_TOKEN}`, Accept: "application/json" }, timeout: 10000 } ); if (commitsResponse.ok) { const commits = await commitsResponse.json(); for (const commit of commits) { const date = new Date(commit.commit.author.date) .toISOString() .split("T")[0]; contributionsByDay.set( date, (contributionsByDay.get(date) || 0) + 1 ); } } } catch (error) { if ( error instanceof NetworkError || error instanceof TimeoutError ) { console.warn( `Network error fetching commits for ${repo.name}, skipping` ); } else { console.error(`Error fetching commits for ${repo.name}:`, error); } } } const contributions: ContributionDay[] = Array.from( contributionsByDay.entries() ).map(([date, count]) => ({ date, count })); return contributions; }, { maxStaleMs: CACHE_CONFIG.GIT_ACTIVITY_MAX_STALE_MS } ).catch((error) => { if (error instanceof NetworkError) { console.error("Gitea API unavailable (network error)"); } else if (error instanceof TimeoutError) { console.error(`Gitea API timeout after ${error.timeoutMs}ms`); } else if (error instanceof APIError) { console.error(`Gitea API error: ${error.status} ${error.statusText}`); } else { console.error("Unexpected error fetching Gitea activity:", error); } return []; }); }) });