interface Follow { did: string; handle: string; displayName: string; avatar: string; associated: { activitySubsription:{ allowSubscriptions: string; } } labels: unknown[] createdAt: string; description: string; indexedAt: string; } interface MiniDoc { did: string; handle: string; pds: string; signing_key: string; } interface Post { uri: string; cid: string; value: { text: string; $type: "app.bsky.feed.post" langs: string[] reply: { root: { cid: string; uri: string; }, parent: { cid: string; uri: string; } } createdAt: string; } } const headers = { 'User-Agent': 'swab/https://tangled.org/dane.is.extraordinarily.cool/swab' } const MONTHS_IN_DAYS = 183 // 6 months, seems reasonable // https://stackoverflow.com/questions/3224834/get-difference-between-2-dates-in-javascript function getDateDifferenceInDays(start: Date, end: Date) { const MS_PER_DAY = 1000 * 60 * 60 * 24; const startUTCDate = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate()); const endUTCDate = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()); return Math.floor((endUTCDate - startUTCDate) / MS_PER_DAY); } async function resolveIdentity(indentifier: string): Promise { try { const response = await fetch(`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${indentifier}`, {headers}) if (!response.ok) { // throw new Error(`Failed to fetch minidoc for ${indentifier}. Status - ${response.status}`) console.error(`Failed to fetch minidoc for ${indentifier}. Status - ${response.status}`) } const data = await response.json() as MiniDoc return data } catch (error) { if (error instanceof Error) { console.error(error.message) } throw error; } } async function getRecentPost(doc: MiniDoc) { try { const response = await fetch(`${doc.pds}/xrpc/com.atproto.repo.listRecords?repo=${doc.did}&collection=app.bsky.feed.post&limit=1`, {headers}) if (!response.ok) { // throw new Error(`There was an error fetching posts for ${doc.handle}. Status - ${response.status}`) console.error(`There was an error fetching posts for ${doc.handle}. Status - ${response.status}`) } const data = await response.json() as {records: Post[]} return data; } catch (error) { if (error instanceof Error) { console.error(error.message) } throw error; } } async function getFollowsByUser(did: string, cursor?: string) { try { const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=${did}&cursor=${cursor}&limit=100`) if (!response.ok) { // throw new Error(`There was a problem getting follows for ${did}. Status - ${response.status}`) console.error(`There was a problem getting follows for ${did}. Status - ${response.status}`) } const data = await response.json() as { cursor: string follows: Follow[] } return { cursor: data?.cursor, follows: data?.follows } } catch (error) { if (error instanceof Error) { console.error(error.message) } throw error; } } let cursor: string | undefined; const unfollowMap = new Map(); do { const {follows, cursor: followsCursor} = await getFollowsByUser("did:plc:qttsv4e7pu2jl3ilanfgc3zn", cursor) for (const [index, follower] of follows.entries()) { const doc = await resolveIdentity(follower.did) const post = await getRecentPost(doc) // it's possible that someone has never made a post i guess, we should add them to the list if (!post?.records?.[0]) { // neg 1 can represent never made post unfollowMap.set(follower.handle, -1) } if (post?.records[0] && post?.records[0]?.value) { const recentPostCreationDate = post?.records?.[0].value?.createdAt // invalid date for some reason idk const daysSinceLastPost = getDateDifferenceInDays(new Date(recentPostCreationDate), new Date()) if (daysSinceLastPost >= MONTHS_IN_DAYS) { unfollowMap.set(follower.handle, daysSinceLastPost) } console.clear(); console.info(`Auditing user [${index + 1} / ${follows.length}]`) } // await new Promise((resolve) => setTimeout(resolve, 1000)) } cursor = followsCursor } while (cursor) console.log(unfollowMap)