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)