bsky follow audit script
1interface Follow {
2 did: string;
3 handle: string;
4 displayName: string;
5 avatar: string;
6 associated: {
7 activitySubsription:{
8 allowSubscriptions: string;
9 }
10 }
11 labels: unknown[]
12 createdAt: string;
13 description: string;
14 indexedAt: string;
15}
16
17interface MiniDoc {
18 did: string;
19 handle: string;
20 pds: string;
21 signing_key: string;
22}
23
24interface Post {
25 uri: string;
26 cid: string;
27 value: {
28 text: string;
29 $type: "app.bsky.feed.post"
30 langs: string[]
31 reply: {
32 root: {
33 cid: string;
34 uri: string;
35 },
36 parent: {
37 cid: string;
38 uri: string;
39 }
40 }
41 createdAt: string;
42 }
43}
44
45const headers = {
46 'User-Agent': 'swab/https://tangled.org/dane.is.extraordinarily.cool/swab'
47}
48
49const MONTHS_IN_DAYS = 183 // 6 months, seems reasonable
50
51
52// https://stackoverflow.com/questions/3224834/get-difference-between-2-dates-in-javascript
53function getDateDifferenceInDays(start: Date, end: Date) {
54 const MS_PER_DAY = 1000 * 60 * 60 * 24;
55 const startUTCDate = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate());
56 const endUTCDate = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate());
57 return Math.floor((endUTCDate - startUTCDate) / MS_PER_DAY);
58}
59
60async function resolveIdentity(indentifier: string): Promise<MiniDoc> {
61 try {
62 const response = await fetch(`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${indentifier}`, {headers})
63 if (!response.ok) {
64 // throw new Error(`Failed to fetch minidoc for ${indentifier}. Status - ${response.status}`)
65 console.error(`Failed to fetch minidoc for ${indentifier}. Status - ${response.status}`)
66 }
67 const data = await response.json() as MiniDoc
68 return data
69 } catch (error) {
70 if (error instanceof Error) {
71 console.error(error.message)
72 }
73 throw error;
74 }
75}
76
77async function getRecentPost(doc: MiniDoc) {
78 try {
79 const response = await fetch(`${doc.pds}/xrpc/com.atproto.repo.listRecords?repo=${doc.did}&collection=app.bsky.feed.post&limit=1`, {headers})
80 if (!response.ok) {
81 // throw new Error(`There was an error fetching posts for ${doc.handle}. Status - ${response.status}`)
82 console.error(`There was an error fetching posts for ${doc.handle}. Status - ${response.status}`)
83 }
84 const data = await response.json() as {records: Post[]}
85 return data;
86 } catch (error) {
87 if (error instanceof Error) {
88 console.error(error.message)
89 }
90
91 throw error;
92 }
93}
94
95async function getFollowsByUser(did: string, cursor?: string) {
96 try {
97 const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${did}&cursor=${cursor}&limit=100`)
98 if (!response.ok) {
99 // throw new Error(`There was a problem getting follows for ${did}. Status - ${response.status}`)
100 console.error(`There was a problem getting follows for ${did}. Status - ${response.status}`)
101 }
102
103 const data = await response.json() as {
104 cursor: string
105 follows: Follow[]
106 }
107
108 return {
109 cursor: data?.cursor,
110 follows: data?.follows
111 }
112 } catch (error) {
113 if (error instanceof Error) {
114 console.error(error.message)
115 }
116 throw error;
117 }
118}
119
120let cursor: string | undefined;
121
122const unfollowMap = new Map<string, number>();
123
124do {
125 const {follows, cursor: followsCursor} = await getFollowsByUser("did:plc:qttsv4e7pu2jl3ilanfgc3zn", cursor)
126 for (const [index, follower] of follows.entries()) {
127 const doc = await resolveIdentity(follower.did)
128 const post = await getRecentPost(doc)
129
130
131 // it's possible that someone has never made a post i guess, we should add them to the list
132 if (!post?.records?.[0]) {
133 // neg 1 can represent never made post
134 unfollowMap.set(follower.handle, -1)
135 }
136 if (post?.records[0] && post?.records[0]?.value) {
137 const recentPostCreationDate = post?.records?.[0].value?.createdAt
138 // invalid date for some reason idk
139 const daysSinceLastPost = getDateDifferenceInDays(new Date(recentPostCreationDate), new Date())
140 if (daysSinceLastPost >= MONTHS_IN_DAYS) {
141 unfollowMap.set(follower.handle, daysSinceLastPost)
142 }
143
144 console.clear();
145 console.info(`Auditing user [${index + 1} / ${follows.length}]`)
146 }
147
148 // await new Promise((resolve) => setTimeout(resolve, 1000))
149}
150cursor = followsCursor
151} while (cursor)
152
153
154console.log(unfollowMap)