this repo has no description
at ari/CleanAndMaintenance 244 lines 6.8 kB view raw
1import { simpleFetchHandler, XRPC } from "@atcute/client"; 2import "@atcute/bluesky/lexicons"; 3import type { 4 AppBskyActorDefs, 5 AppBskyActorProfile, 6 AppBskyFeedPost, 7 At, 8 ComAtprotoRepoListRecords, 9} from "@atcute/client/lexicons"; 10import { 11 CompositeDidDocumentResolver, 12 PlcDidDocumentResolver, 13 WebDidDocumentResolver, 14} from "@atcute/identity-resolver"; 15import { Config } from "../../config"; 16// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; 17// import { AppBskyFeedPost } from "@atcute/client/lexicons"; 18// import { AppBskyActorDefs } from "@atcute/client/lexicons"; 19 20interface AccountMetadata { 21 did: string; 22 displayName: string; 23 handle: string; 24 avatarCid: string | null; 25} 26interface atUriObject { 27 repo: string; 28 collection: string; 29 rkey: string; 30} 31class Post { 32 authorDid: string; 33 authorAvatarCid: string | null; 34 postCid: string; 35 recordName: string; 36 authorHandle: string; 37 displayName: string; 38 text: string; 39 timestamp: number; 40 timenotstamp: string; 41 quotingUri: atUriObject | null; 42 replyingUri: atUriObject | null; 43 imagesCid: string[] | null; 44 videosLinkCid: string | null; 45 46 constructor( 47 record: ComAtprotoRepoListRecords.Record, 48 account: AccountMetadata, 49 ) { 50 this.postCid = record.cid; 51 this.recordName = processAtUri(record.uri).rkey; 52 this.authorDid = account.did; 53 this.authorAvatarCid = account.avatarCid; 54 this.authorHandle = account.handle; 55 this.displayName = account.displayName; 56 const post = record.value as AppBskyFeedPost.Record; 57 this.timenotstamp = post.createdAt; 58 this.text = post.text; 59 this.timestamp = Date.parse(post.createdAt); 60 if (post.reply) { 61 this.replyingUri = processAtUri(post.reply.parent.uri); 62 } else { 63 this.replyingUri = null; 64 } 65 this.quotingUri = null; 66 this.imagesCid = null; 67 this.videosLinkCid = null; 68 switch (post.embed?.$type) { 69 case "app.bsky.embed.images": 70 this.imagesCid = post.embed.images.map((imageRecord: any) => 71 imageRecord.image.ref.$link 72 ); 73 break; 74 case "app.bsky.embed.video": 75 this.videosLinkCid = post.embed.video.ref.$link; 76 break; 77 case "app.bsky.embed.record": 78 this.quotingUri = processAtUri(post.embed.record.uri); 79 break; 80 case "app.bsky.embed.recordWithMedia": 81 this.quotingUri = processAtUri(post.embed.record.record.uri); 82 switch (post.embed.media.$type) { 83 case "app.bsky.embed.images": 84 this.imagesCid = post.embed.media.images.map((imageRecord) => 85 imageRecord.image.ref.$link 86 ); 87 88 break; 89 case "app.bsky.embed.video": 90 this.videosLinkCid = post.embed.media.video.ref.$link; 91 92 break; 93 } 94 break; 95 } 96 } 97} 98 99const processAtUri = (aturi: string): atUriObject => { 100 const parts = aturi.split("/"); 101 return { 102 repo: parts[2], 103 collection: parts[3], 104 rkey: parts[4], 105 }; 106}; 107 108const rpc = new XRPC({ 109 handler: simpleFetchHandler({ 110 service: Config.PDS_URL, 111 }), 112}); 113 114const getDidsFromPDS = async () : Promise<At.Did[]> => { 115 const { data } = await rpc.get("com.atproto.sync.listRepos", { 116 params: {}, 117 }); 118 return data.repos.map((repo: any) => (repo.did)) as At.Did[]; 119}; 120const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<AccountMetadata> => { 121 // gonna assume self exists in the app.bsky.actor.profile 122 try { 123 const { data } = await rpc.get("com.atproto.repo.getRecord", { 124 params: { 125 repo: did, 126 collection: "app.bsky.actor.profile", 127 rkey: "self", 128 }, 129 }); 130 const value = data.value as AppBskyActorProfile.Record; 131 const handle = await blueskyHandleFromDid(did); 132 const account: AccountMetadata = { 133 did: did, 134 handle: handle, 135 displayName: value.displayName || "", 136 avatarCid: null, 137 }; 138 if (value.avatar) { 139 account.avatarCid = value.avatar.ref["$link"]; 140 } 141 return account; 142 } 143 catch (e) { 144 console.error(`Error fetching metadata for ${did}:`, e); 145 return { 146 did: "error", 147 displayName: "", 148 avatarCid: null, 149 handle: "error", 150 }; 151 } 152}; 153 154const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => { 155 const dids = await getDidsFromPDS(); 156 const metadata = await Promise.all( 157 dids.map(async (repo: `did:${string}:${string}`) => { 158 return await getAccountMetadata(repo); 159 }), 160 ); 161 return metadata.filter(account => account.did !== "error"); 162}; 163 164const fetchPosts = async (did: string) => { 165 try { 166 const { data } = await rpc.get("com.atproto.repo.listRecords", { 167 params: { 168 repo: did as At.Identifier, 169 collection: "app.bsky.feed.post", 170 limit: Config.MAX_POSTS, 171 }, 172 }); 173 return { 174 records: data.records as ComAtprotoRepoListRecords.Record[], 175 did: did, 176 error: false 177 }; 178 } catch (e) { 179 console.error(`Error fetching posts for ${did}:`, e); 180 return { 181 records: [], 182 did: did, 183 error: true 184 }; 185 } 186}; 187 188const identityResolve = async (did: At.Did) => { 189 const resolver = new CompositeDidDocumentResolver({ 190 methods: { 191 plc: new PlcDidDocumentResolver(), 192 web: new WebDidDocumentResolver(), 193 }, 194 }); 195 196 if (did.startsWith("did:plc:") || did.startsWith("did:web:")) { 197 const doc = await resolver.resolve( 198 did as `did:plc:${string}` | `did:web:${string}`, 199 ); 200 return doc; 201 } else { 202 throw new Error(`Unsupported DID type: ${did}`); 203 } 204}; 205 206const blueskyHandleFromDid = async (did: At.Did) => { 207 const doc = await identityResolve(did); 208 if (doc.alsoKnownAs) { 209 const handleAtUri = doc.alsoKnownAs.find((url) => url.startsWith("at://")); 210 const handle = handleAtUri?.split("/")[2]; 211 if (!handle) { 212 return "Handle not found"; 213 } else { 214 return handle; 215 } 216 } else { 217 return "Handle not found"; 218 } 219}; 220 221const fetchAllPosts = async () => { 222 const users: AccountMetadata[] = await getAllMetadataFromPds(); 223 const postRecords = await Promise.all( 224 users.map(async (metadata: AccountMetadata) => 225 await fetchPosts(metadata.did) 226 ), 227 ); 228 const validPostRecords = postRecords.filter(record => !record.error); 229 const posts: Post[] = validPostRecords.flatMap((userFetch) => 230 userFetch.records.map((record) => { 231 const user = users.find((user: AccountMetadata) => 232 user.did == userFetch.did 233 ); 234 if (!user) { 235 throw new Error(`User with DID ${userFetch.did} not found`); 236 } 237 return new Post(record, user); 238 }) 239 ); 240 posts.sort((a, b) => b.timestamp - a.timestamp); 241 return posts.slice(0, Config.MAX_POSTS); 242}; 243export { fetchAllPosts, getAllMetadataFromPds, Post }; 244export type { AccountMetadata };