import { get, writable } from 'svelte/store'; import { parseFeed } from '@rowanmanning/feed-parser'; export const ACTIVITY_PREVIEW_LIMIT = 7; const lastCommits = writable([]); export const updateCommits = async () => { try { const githubFeed = await parseFeedToActivity('https://github.com/90-008.atom'); const codebergFeed = await parseFeedToActivity('https://codeberg.org/90-008.atom'); const tangledFeed = await fetchTangledActivity(); const mergedFeed = sortActivities(githubFeed.concat(codebergFeed).concat(tangledFeed)); lastCommits.set(mergedFeed); } catch (why) { console.log('could not fetch git activity: ', why); } }; export const getLastActivity = () => { return get(lastCommits).slice(0, ACTIVITY_PREVIEW_LIMIT); }; export const getCurrentActivity = () => { return get(lastCommits); }; export const activityToJson = (activity: Activity): ActivityJson => { return { ...activity, date: activity.date?.toISOString() ?? null }; }; export const currentActivityToJson = (activities: Activity[]) => { return activities.map(activityToJson); }; export type Activity = { source: string; description: string; link: string | null; date: Date | null; id?: string; }; export type ActivityJson = Omit & { date: string | null; }; const toHex = (bytes: number[]): string => { return bytes.map((b) => b.toString(16).padStart(2, '0')).join(''); }; const fetchTangledActivity = async (): Promise => { // todo: auto resolve pds and knots const did = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; const pds = 'https://zwsp.xyz'; const knot = 'https://knot.gaze.systems'; const activities: Activity[] = []; try { // todo: fetch until we exhaust const listRes = await fetch( `${pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=sh.tangled.repo` ); if (!listRes.ok) return []; const listData = await listRes.json(); for (const record of listData.records || []) { const repoName = record.value.name; if (!repoName) continue; try { const logRes = await fetch(`${knot}/xrpc/sh.tangled.repo.log?repo=${did}/${repoName}`); if (!logRes.ok) continue; const logData = await logRes.json(); const commits = logData.commits || []; for (const commit of commits) { if (!commit.hash) continue; const hash = commit.hash ? toHex(commit.hash) : ''; if (activities.some((a) => a.id === hash)) continue; const message = commit.message || ''; const dateStr = commit.author?.When; activities.push({ source: 'tangled', description: `${repoName}: ${message}`, link: `https://tangled.sh/${did}/${repoName}/commit/${hash}`, date: dateStr ? new Date(dateStr) : null, id: hash }); } } catch (err) { console.log(`could not fetch tangled log for ${repoName}:`, err); } } } catch (err) { console.log('could not fetch tangled repos:', err); } return activities; }; const parseFeedToActivity = async (url: string) => { const response = await fetch(url); const feed = parseFeed(await response.text()); const source = new URL(url).host.split('.')[0]; const results: Activity[] = []; for (const item of feed.items) { const description: string | null = item.description || item.title; if (description === null) continue; // dont count mirrored repos // TODO: probably can implement a deduplication algorithm if ( source === 'github' && [ '90-008/ark', '90-008/website', 'ark', 'website', 'trill', 'faunu', 'nucleus', 'hydrant' ].some((repo) => description.includes(repo)) ) continue; // dont show activity that is just chore if (item.content?.includes('chore')) continue; let repoName = ''; let message = ''; let link = item.url; if (source === 'github') { // try to extract repo from url // url format: https://github.com/user/repo/... try { const url = new URL(item.url || ''); const parts = url.pathname.split('/').filter(Boolean); if (parts.length >= 2) { repoName = parts[1]; // just the repo name, e.g. "endpoint" } } catch { /* empty */ } // try to extract commit message from content blockquote if (item.content) { const match = item.content.match(/
(.*?)<\/blockquote>/s); if (match && match[1]) { message = match[1].trim(); } } } // fallback or original logic for non-github or failed parsing if (!message || !repoName) { const desc = description.split('').at(1) || description.split('').pop() || ''; if (!message) message = desc.replace(/^90-008 /, ''); // If we couldn't get a clean repo name, we might leave it empty or try to parse from description if needed // But for now let's stick to what we found or the original description cleanup } results.push({ source, description: repoName && message ? `${repoName}: ${message}` : message, link, date: item.published || item.updated }); } return results; }; const sortActivities = (activities: Array) => { return activities.sort((a, b) => { if (a.date === null && b.date === null) return 0; if (a.date === null) return 1; if (b.date === null) return -1; return b.date.getTime() - a.date.getTime(); }); };