this repo has no description
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 };