1import { get, writable } from 'svelte/store';
2import { parseFeed } from '@rowanmanning/feed-parser';
3
4const lastCommits = writable<Activity[]>([]);
5
6export const updateCommits = async () => {
7 try {
8 const githubFeed = await parseFeedToActivity('https://github.com/90-008.atom');
9 const codebergFeed = await parseFeedToActivity('https://codeberg.org/90-008.atom');
10 const tangledFeed = await fetchTangledActivity();
11 const mergedFeed = sortActivities(
12 githubFeed.concat(codebergFeed).concat(tangledFeed)
13 ).slice(0, 7);
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);
22};
23
24type Activity = {
25 source: string;
26 description: string;
27 link: string | null;
28 date: Date | null;
29};
30
31const toHex = (bytes: number[]): string => {
32 return bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
33};
34
35const fetchTangledActivity = async (): Promise<Activity[]> => {
36 // todo: auto resolve pds and knots
37 const did = 'did:plc:dfl62fgb7wtjj3fcbb72naae';
38 const pds = 'https://zwsp.xyz';
39 const knot = 'https://knot.gaze.systems';
40 const activities: Activity[] = [];
41
42 try {
43 // todo: fetch until we exhaust
44 const listRes = await fetch(
45 `${pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=sh.tangled.repo`
46 );
47 if (!listRes.ok) return [];
48 const listData = await listRes.json();
49
50 for (const record of listData.records || []) {
51 const repoName = record.value.name;
52 if (!repoName) continue;
53
54 try {
55 const logRes = await fetch(
56 `${knot}/xrpc/sh.tangled.repo.log?repo=${did}/${repoName}`
57 );
58 if (!logRes.ok) continue;
59 const logData = await logRes.json();
60
61 const commits = logData.commits || [];
62
63 for (const commit of commits) {
64 const hash = commit.Hash ? toHex(commit.Hash) : '';
65 const message = commit.Message || '';
66 const dateStr = commit.Author?.When;
67
68 activities.push({
69 source: 'tangled',
70 description: `pushed ${repoName}: ${message}`,
71 link: `https://tangled.sh/${did}/${repoName}/commit/${hash}`,
72 date: dateStr ? new Date(dateStr) : null
73 });
74 }
75 } catch (err) {
76 console.log(`could not fetch tangled log for ${repoName}:`, err);
77 }
78 }
79 } catch (err) {
80 console.log('could not fetch tangled repos:', err);
81 }
82 return activities;
83};
84
85const parseFeedToActivity = async (url: string) => {
86 const response = await fetch(url);
87 const feed = parseFeed(await response.text());
88
89 const source = new URL(url).host.split('.')[0];
90 const results: Activity[] = [];
91 for (const item of feed.items) {
92 const description: string | null = item.description || item.title;
93 if (description === null) continue;
94 // dont count mirrored repos
95 // TODO: probably can implement a deduplication algorithm
96 if (
97 ['90-008/ark', '90-008/website', 'ark', 'website'].some((repo) =>
98 description.includes(repo)
99 )
100 )
101 continue;
102 // dont show activity that is just chore
103 if (item.content?.includes('chore')) continue;
104 const desc = description.split('</a>').at(1) || description.split('</a>').pop() || '';
105 results.push({
106 source,
107 description: desc.replace(/^90-008 /, ""),
108 link: item.url,
109 date: item.published || item.updated
110 });
111 }
112
113 return results;
114};
115
116const sortActivities = (activities: Array<Activity>) => {
117 return activities.sort((a, b) => {
118 if (a.date === null && b.date === null) return 0;
119 if (a.date === null) return 1;
120 if (b.date === null) return -1;
121 return b.date.getTime() - a.date.getTime();
122 });
123};