[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Database } from "../db/index.ts";
2import { TimeCidKeyset } from "../db/pagination.ts";
3
4// Helper function to format feed items
5function feedItemFromRow(
6 item: { uri: string; cid: string },
7): { uri: string; cid: string } {
8 return {
9 uri: item.uri,
10 cid: item.cid,
11 };
12}
13
14interface FeedItem {
15 uri: string;
16 cid: string;
17 authorDid: string;
18 createdAt: string;
19 indexedAt: string;
20}
21
22export class Feeds {
23 private db: Database;
24 private timeCidKeyset: TimeCidKeyset;
25
26 constructor(db: Database) {
27 this.db = db;
28 this.timeCidKeyset = new TimeCidKeyset();
29 }
30
31 async getFeedGenerators(uris: string[]) {
32 if (!uris.length) return { generators: [] };
33
34 const generators = await this.db.models.Generator.find({
35 uri: { $in: uris },
36 }).populate("actor");
37
38 return {
39 generators: generators.map((generator) => ({
40 uri: generator.uri,
41 cid: generator.cid,
42 authorDid: generator.authorDid,
43 displayName: generator.displayName,
44 description: generator.description,
45 descriptionFacets: generator.descriptionFacets,
46 avatar: generator.avatar,
47 acceptsInteractions: generator.acceptsInteractions,
48 likeCount: generator.likeCount || 0,
49 createdAt: generator.createdAt,
50 indexedAt: generator.indexedAt,
51 actor: generator.actor,
52 })),
53 };
54 }
55
56 async getAuthorFeed(
57 actorDid: string,
58 limit = 50,
59 cursor?: string,
60 ) {
61 // Get posts by this author - Post collection doesn't have replies (they're in Reply collection)
62 const postsQuery = this.db.models.Post.find({
63 authorDid: actorDid,
64 });
65
66 // Apply pagination to posts query
67 const paginatedPostsQuery = this.timeCidKeyset.paginate(postsQuery, {
68 limit,
69 cursor,
70 direction: "desc",
71 });
72
73 const posts = await paginatedPostsQuery.exec();
74
75 // Transform posts
76 const transformedPosts: FeedItem[] = posts.map((p) => ({
77 uri: p.uri,
78 cid: p.cid,
79 authorDid: p.authorDid,
80 createdAt: p.createdAt,
81 indexedAt: p.indexedAt,
82 }));
83
84 return {
85 items: transformedPosts.map(feedItemFromRow),
86 cursor: this.timeCidKeyset.packFromResult(transformedPosts),
87 };
88 }
89
90 async getTimeline(actorDid: string, limit = 50, cursor?: string) {
91 // Get people this actor follows
92 const follows = await this.db.models.Follow.find({ authorDid: actorDid });
93
94 const followedDids = follows.map((f) => f.subject);
95 const timelineDids = [...followedDids, actorDid];
96
97 // Get timeline posts
98 const postsQuery = this.db.models.Post.find({
99 authorDid: { $in: timelineDids },
100 });
101
102 // Apply pagination using createdAt + cid (which matches DB schema and indexes)
103 const paginatedPostsQuery = this.timeCidKeyset.paginate(postsQuery, {
104 limit,
105 cursor,
106 direction: "desc",
107 });
108
109 const posts = await paginatedPostsQuery.exec();
110
111 // Transform posts
112 const transformedPosts: FeedItem[] = posts.map((p) => ({
113 uri: p.uri,
114 cid: p.cid,
115 authorDid: p.authorDid,
116 createdAt: p.createdAt,
117 indexedAt: p.indexedAt,
118 }));
119
120 return {
121 items: transformedPosts.map(feedItemFromRow),
122 cursor: this.timeCidKeyset.packFromResult(transformedPosts),
123 };
124 }
125}