data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at svelte 188 lines 5.3 kB view raw
1import { get, writable } from 'svelte/store'; 2import { parseFeed } from '@rowanmanning/feed-parser'; 3 4export const ACTIVITY_PREVIEW_LIMIT = 7; 5 6const lastCommits = writable<Activity[]>([]); 7 8export const updateCommits = async () => { 9 try { 10 const githubFeed = await parseFeedToActivity('https://github.com/90-008.atom'); 11 const codebergFeed = await parseFeedToActivity('https://codeberg.org/90-008.atom'); 12 const tangledFeed = await fetchTangledActivity(); 13 const mergedFeed = sortActivities(githubFeed.concat(codebergFeed).concat(tangledFeed)); 14 lastCommits.set(mergedFeed); 15 } catch (why) { 16 console.log('could not fetch git activity: ', why); 17 } 18}; 19 20export const getLastActivity = () => { 21 return get(lastCommits).slice(0, ACTIVITY_PREVIEW_LIMIT); 22}; 23 24export const getCurrentActivity = () => { 25 return get(lastCommits); 26}; 27 28export const activityToJson = (activity: Activity): ActivityJson => { 29 return { 30 ...activity, 31 date: activity.date?.toISOString() ?? null 32 }; 33}; 34 35export const currentActivityToJson = (activities: Activity[]) => { 36 return activities.map(activityToJson); 37}; 38 39export type Activity = { 40 source: string; 41 description: string; 42 link: string | null; 43 date: Date | null; 44 id?: string; 45}; 46 47export type ActivityJson = Omit<Activity, 'date'> & { 48 date: string | null; 49}; 50 51const toHex = (bytes: number[]): string => { 52 return bytes.map((b) => b.toString(16).padStart(2, '0')).join(''); 53}; 54 55const fetchTangledActivity = async (): Promise<Activity[]> => { 56 // todo: auto resolve pds and knots 57 const did = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; 58 const pds = 'https://zwsp.xyz'; 59 const knot = 'https://knot.gaze.systems'; 60 const activities: Activity[] = []; 61 62 try { 63 // todo: fetch until we exhaust 64 const listRes = await fetch( 65 `${pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=sh.tangled.repo` 66 ); 67 if (!listRes.ok) return []; 68 const listData = await listRes.json(); 69 70 for (const record of listData.records || []) { 71 const repoName = record.value.name; 72 if (!repoName) continue; 73 74 try { 75 const logRes = await fetch(`${knot}/xrpc/sh.tangled.repo.log?repo=${did}/${repoName}`); 76 if (!logRes.ok) continue; 77 const logData = await logRes.json(); 78 79 const commits = logData.commits || []; 80 81 for (const commit of commits) { 82 if (!commit.hash) continue; 83 84 const hash = commit.hash ? toHex(commit.hash) : ''; 85 if (activities.some((a) => a.id === hash)) continue; 86 87 const message = commit.message || ''; 88 const dateStr = commit.author?.When; 89 90 activities.push({ 91 source: 'tangled', 92 description: `${repoName}: ${message}`, 93 link: `https://tangled.sh/${did}/${repoName}/commit/${hash}`, 94 date: dateStr ? new Date(dateStr) : null, 95 id: hash 96 }); 97 } 98 } catch (err) { 99 console.log(`could not fetch tangled log for ${repoName}:`, err); 100 } 101 } 102 } catch (err) { 103 console.log('could not fetch tangled repos:', err); 104 } 105 return activities; 106}; 107 108const parseFeedToActivity = async (url: string) => { 109 const response = await fetch(url); 110 const feed = parseFeed(await response.text()); 111 112 const source = new URL(url).host.split('.')[0]; 113 const results: Activity[] = []; 114 for (const item of feed.items) { 115 const description: string | null = item.description || item.title; 116 if (description === null) continue; 117 // dont count mirrored repos 118 // TODO: probably can implement a deduplication algorithm 119 if ( 120 source === 'github' && 121 [ 122 '90-008/ark', 123 '90-008/website', 124 'ark', 125 'website', 126 'trill', 127 'faunu', 128 'nucleus', 129 'hydrant' 130 ].some((repo) => description.includes(repo)) 131 ) 132 continue; 133 // dont show activity that is just chore 134 if (item.content?.includes('chore')) continue; 135 136 let repoName = ''; 137 let message = ''; 138 let link = item.url; 139 140 if (source === 'github') { 141 // try to extract repo from url 142 // url format: https://github.com/user/repo/... 143 try { 144 const url = new URL(item.url || ''); 145 const parts = url.pathname.split('/').filter(Boolean); 146 if (parts.length >= 2) { 147 repoName = parts[1]; // just the repo name, e.g. "endpoint" 148 } 149 } catch { 150 /* empty */ 151 } 152 153 // try to extract commit message from content blockquote 154 if (item.content) { 155 const match = item.content.match(/<blockquote>(.*?)<\/blockquote>/s); 156 if (match && match[1]) { 157 message = match[1].trim(); 158 } 159 } 160 } 161 162 // fallback or original logic for non-github or failed parsing 163 if (!message || !repoName) { 164 const desc = description.split('</a>').at(1) || description.split('</a>').pop() || ''; 165 if (!message) message = desc.replace(/^90-008 /, ''); 166 // If we couldn't get a clean repo name, we might leave it empty or try to parse from description if needed 167 // But for now let's stick to what we found or the original description cleanup 168 } 169 170 results.push({ 171 source, 172 description: repoName && message ? `${repoName}: ${message}` : message, 173 link, 174 date: item.published || item.updated 175 }); 176 } 177 178 return results; 179}; 180 181const sortActivities = (activities: Array<Activity>) => { 182 return activities.sort((a, b) => { 183 if (a.date === null && b.date === null) return 0; 184 if (a.date === null) return 1; 185 if (b.date === null) return -1; 186 return b.date.getTime() - a.date.getTime(); 187 }); 188};