Something went wrong. Try again.
My website. cameron.stream
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340import { load } from "cheerio";import { cacheGet, cacheSet } from "./cache.ts";
export interface ActivityDay { date: string; count: number; level: number;}
export interface ActivitySeries { total: number; period: string; days: ActivityDay[];}
export interface TangledRepository { repoDid: string; name: string; description: string; website: string | null; url: string;}
export interface TangledCommit { subject: string; repository: string; time: string; url: string;}
export interface CodeActivityData { github: ActivitySeries | null; tangled: ActivitySeries | null; tangledPinned: TangledRepository[]; tangledRecent: TangledCommit[];}
interface DidDocument { service?: Array<{ type?: string; serviceEndpoint?: string; }>;}
interface AtRecord<T = Record<string, unknown>> { uri: string; value: T;}
interface TangledProfileRecord { pinnedRepositories?: unknown;}
interface TangledRepoRecord { repoDid?: unknown; name?: unknown; description?: unknown; website?: unknown; createdAt?: unknown;}
interface GetRecordResponse<T> { value?: T;}
interface ListRecordsResponse<T> { records?: Array<AtRecord<T>>;}
const CAMERON_DID = "did:plc:gfrmhdmjvxn2sjedzboeudef";const TANGLED_HANDLE = "cameron.stream";const TID_RKEY = /^3[a-z2-7]{12}$/;
const REQUEST_HEADERS = { "User-Agent": "cameron.stream code activity", Accept: "text/html",};
function contributionCount(label: string): number { if (/^no contributions/i.test(label.trim())) return 0; const match = label.match(/([\d,]+) contribution/i); return match ? Number(match[1].replaceAll(",", "")) : 0;}
export function parseGithubActivity(html: string): ActivitySeries | null { const totalMatch = html.match( /<h2[^>]*id="js-contribution-activity-description"[^>]*>[\s\S]*?([\d,]+)[\s\S]*?contributions[\s\S]*?<\/h2>/i ); const days: ActivityDay[] = []; const dayPattern = /<td[^>]*data-date="([^"]+)"[^>]*data-level="([0-4])"[^>]*><\/td>\s*<tool-tip[^>]*>([^<]+)<\/tool-tip>/gi;
for (const match of html.matchAll(dayPattern)) { days.push({ date: match[1], level: Number(match[2]), count: contributionCount(match[3]), }); }
if (!totalMatch || days.length < 300) return null; days.sort((a, b) => a.date.localeCompare(b.date));
return { total: Number(totalMatch[1].replaceAll(",", "")), period: "in the last year", days, };}
function tangledLevel(count: number): number { if (count === 0) return 0; if (count === 1) return 1; if (count <= 3) return 2; if (count <= 7) return 3; return 4;}
// Resolve the default branch from the repository landing page's own history// navigation, never from README links or an undocumented ATProto record field.export function parseTangledHistoryUrl(html: string, repositoryUrl: string): string | null { const $ = load(html); const href = $("#commit-log a[href]").first().attr("href"); if (!href || !URL.canParse(href, "https://tangled.org") || !URL.canParse(repositoryUrl)) return null; const url = new URL(href, "https://tangled.org"); const repository = new URL(repositoryUrl); const prefix = `${repository.pathname}/commits/`; return url.origin === "https://tangled.org" && url.pathname.startsWith(prefix) && url.pathname.length > prefix.length && !url.search && !url.hash ? url.href : null;}
export function parseTangledRecentCommits(html: string, limit = 6): TangledCommit[] { const $ = load(html); const commits = new Map<string, TangledCommit>(); // Each history row includes a hash link, a nested subject link and a sibling // time column. Restrict all extraction to that row, including missing times. for (const row of $("div.grid-cols-14").toArray()) { const item = $(row); const time = item.find("time[datetime]").attr("datetime"); if (!time || Number.isNaN(Date.parse(time))) continue; for (const anchor of item.find(".col-span-6 a[href]").toArray()) { const link = $(anchor); const path = link.attr("href") ?? ""; const match = path.match(/^\/([^/]+\/[^/]+)\/commit\/([0-9a-f]{40})$/i); const subject = link.text().replace(/\s+/g, " ").trim(); if (!match || !subject) continue; const url = `https://tangled.org${path}`; commits.set(url, { subject, repository: match[1], time, url }); break; } } return [...commits.values()].slice(0, Math.max(0, limit));}
export function parseTangledActivity(html: string): ActivitySeries | null { const totalMatch = html.match(/Punchcard[\s\S]{0,500}?([\d,]+) commits/i); const days: ActivityDay[] = []; const dayPattern = /title="(\d{4}-\d{2}-\d{2}): ([\d,]+) commits"/gi;
for (const match of html.matchAll(dayPattern)) { const count = Number(match[2].replaceAll(",", "")); days.push({ date: match[1], count, level: tangledLevel(count), }); }
if (!totalMatch || days.length < 300) return null; days.sort((a, b) => a.date.localeCompare(b.date));
return { total: Number(totalMatch[1].replaceAll(",", "")), period: `in ${days[0].date.slice(0, 4)}`, days, };}
async function fetchText(url: string): Promise<string | null> { try { const response = await fetch(url, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(8_000), }); return response.ok ? await response.text() : null; } catch { return null; }}
async function fetchJson<T>(url: string): Promise<T | null> { try { const response = await fetch(url, { headers: { "User-Agent": "cameron.stream code activity", Accept: "application/json", }, signal: AbortSignal.timeout(8_000), }); return response.ok ? await response.json() as T : null; } catch { return null; }}
function recordKey(uri: string): string { return uri.slice(uri.lastIndexOf("/") + 1);}
function repositoryName(record: AtRecord<TangledRepoRecord>): string | null { const rkey = recordKey(record.uri); if (!TID_RKEY.test(rkey)) return rkey; return typeof record.value.name === "string" && record.value.name ? record.value.name : null;}
function repositoryNameQuality(record: AtRecord<TangledRepoRecord>): number { return TID_RKEY.test(recordKey(record.uri)) ? 1 : 2;}
function createdAt(record: AtRecord<TangledRepoRecord>): number { if (typeof record.value.createdAt !== "string") return 0; const timestamp = Date.parse(record.value.createdAt); return Number.isNaN(timestamp) ? 0 : timestamp;}
export function parseTangledPinnedRepositories( profile: TangledProfileRecord, records: Array<AtRecord<TangledRepoRecord>>,): TangledRepository[] { if (!Array.isArray(profile.pinnedRepositories)) return [];
return profile.pinnedRepositories.flatMap((repoDid) => { if (typeof repoDid !== "string") return [];
const candidates = records .filter((record) => record.value.repoDid === repoDid) .filter((record) => repositoryName(record) !== null) .sort((a, b) => { const quality = repositoryNameQuality(b) - repositoryNameQuality(a); return quality || createdAt(b) - createdAt(a); }); const selected = candidates[0]; if (!selected) return [];
const name = repositoryName(selected); if (!name) return [];
return [{ repoDid, name, description: typeof selected.value.description === "string" ? selected.value.description : "", website: typeof selected.value.website === "string" && selected.value.website ? selected.value.website : null, url: `https://tangled.org/${TANGLED_HANDLE}/${encodeURIComponent(name)}`, }]; });}
async function getTangledPinnedRepositories(): Promise<TangledRepository[]> { const didDocument = await fetchJson<DidDocument>( `https://plc.directory/${CAMERON_DID}`, ); const pds = didDocument?.service?.find( (service) => service.type === "AtprotoPersonalDataServer", )?.serviceEndpoint; if (!pds) return [];
const query = (collection: string) => `repo=${encodeURIComponent(CAMERON_DID)}&collection=${encodeURIComponent(collection)}`; const [profileResponse, repositoriesResponse] = await Promise.all([ fetchJson<GetRecordResponse<TangledProfileRecord>>( `${pds}/xrpc/com.atproto.repo.getRecord?${query("sh.tangled.actor.profile")}&rkey=self`, ), fetchJson<ListRecordsResponse<TangledRepoRecord>>( `${pds}/xrpc/com.atproto.repo.listRecords?${query("sh.tangled.repo")}&limit=100`, ), ]);
if (!profileResponse?.value || !repositoriesResponse?.records) return []; return parseTangledPinnedRepositories( profileResponse.value, repositoriesResponse.records, );}
export async function fetchPinnedRecentCommits(repositories: TangledRepository[]): Promise<TangledCommit[]> { const commits: TangledCommit[] = []; const pinned = [...new Map(repositories.map((repo) => [repo.url, repo])).values()].slice(0, 4); for (let index = 0; index < pinned.length; index += 2) { const batch = pinned.slice(index, index + 2).map(async (repository) => { const landing = await fetchText(repository.url); const historyUrl = landing ? parseTangledHistoryUrl(landing, repository.url) : null; if (!historyUrl) return []; const html = await fetchText(historyUrl); return html ? parseTangledRecentCommits(html).filter((commit) => commit.url.startsWith(`${repository.url}/commit/`)) : []; }); const results = await Promise.all(batch); commits.push(...results.flat()); } const unique = new Map<string, TangledCommit>(); for (const commit of commits) { const hash = commit.url.slice(commit.url.lastIndexOf("/") + 1); unique.set(`${commit.repository}:${hash}`, commit); } return [...unique.values()] .sort((a, b) => Date.parse(b.time) - Date.parse(a.time)) .slice(0, 6);}
export async function getCodeActivity(): Promise<CodeActivityData> { const cacheKey = "code-activity:v3"; const cached = await cacheGet(cacheKey); if (cached !== undefined) return cached as CodeActivityData;
const [githubHtml, tangledHtml, tangledPinned] = await Promise.all([ fetchText("https://github.com/users/just-cameron/contributions"), fetchText("https://tangled.org/cameron.stream"), getTangledPinnedRepositories(), ]); const activity = { github: githubHtml ? parseGithubActivity(githubHtml) : null, tangled: tangledHtml ? parseTangledActivity(tangledHtml) : null, tangledPinned, tangledRecent: await fetchPinnedRecentCommits(tangledPinned), };
await cacheSet(cacheKey, activity, { life: activity.github && activity.tangled && activity.tangledPinned.length > 0 && activity.tangledRecent.length > 0 ? "hours" : "minutes", }); return activity;}