[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Database } from "../db/index.ts";
2
3// Types for MongoDB aggregation results
4interface AggregationResult {
5 _id: string;
6 count: number;
7}
8
9export class Interactions {
10 private db: Database;
11
12 constructor(db: Database) {
13 this.db = db;
14 }
15
16 async getInteractionCounts(refs: Array<{ uri: string }>) {
17 const uris = refs.map((ref) => ref.uri);
18 if (uris.length === 0) {
19 return { likes: [], replies: [], reposts: [], quotes: [] };
20 }
21
22 // Get pre-computed counts from Post and Reply documents
23 const [posts, replies] = await Promise.all([
24 this.db.models.Post.find(
25 { uri: { $in: uris } },
26 { uri: 1, likeCount: 1, replyCount: 1, repostCount: 1 },
27 ),
28 this.db.models.Reply.find(
29 { uri: { $in: uris } },
30 { uri: 1, likeCount: 1, replyCount: 1, repostCount: 1 },
31 ),
32 ]);
33
34 // Create lookup maps from pre-computed counts
35 const likesMap = new Map<string, number>();
36 const repliesMap = new Map<string, number>();
37 const repostsMap = new Map<string, number>();
38
39 for (const post of posts) {
40 likesMap.set(post.uri, post.likeCount ?? 0);
41 repliesMap.set(post.uri, post.replyCount ?? 0);
42 repostsMap.set(post.uri, post.repostCount ?? 0);
43 }
44
45 for (const reply of replies) {
46 likesMap.set(reply.uri, reply.likeCount ?? 0);
47 repliesMap.set(reply.uri, reply.replyCount ?? 0);
48 }
49
50 return {
51 likes: uris.map((uri) => likesMap.get(uri) ?? 0),
52 replies: uris.map((uri) => repliesMap.get(uri) ?? 0),
53 reposts: uris.map((uri) => repostsMap.get(uri) ?? 0),
54 };
55 }
56
57 async getCountsForUsers(dids: string[]) {
58 if (dids.length === 0) {
59 return {
60 followers: [],
61 following: [],
62 posts: [],
63 feeds: [],
64 };
65 }
66
67 const [followers, following, posts, feeds] = await Promise.all([
68 // Count followers for each DID
69 this.db.models.Follow.aggregate([
70 { $match: { subject: { $in: dids } } },
71 { $group: { _id: "$subject", count: { $sum: 1 } } },
72 ]),
73 // Count following for each DID
74 this.db.models.Follow.aggregate([
75 { $match: { authorDid: { $in: dids } } },
76 { $group: { _id: "$authorDid", count: { $sum: 1 } } },
77 ]),
78 // Count posts for each DID
79 this.db.models.Post.aggregate([
80 { $match: { authorDid: { $in: dids } } },
81 { $group: { _id: "$authorDid", count: { $sum: 1 } } },
82 ]),
83 // Count generators for each DID
84 this.db.models.Generator.aggregate([
85 { $match: { authorDid: { $in: dids } } },
86 { $group: { _id: "$authorDid", count: { $sum: 1 } } },
87 ]),
88 ]);
89
90 // Create lookup maps
91 const followersMap = new Map(
92 followers.map((item: AggregationResult) => [item._id, item.count]),
93 );
94 const followingMap = new Map(
95 following.map((item: AggregationResult) => [item._id, item.count]),
96 );
97 const postsMap = new Map(
98 posts.map((item: AggregationResult) => [item._id, item.count]),
99 );
100 const feedsMap = new Map(
101 feeds.map((item: AggregationResult) => [item._id, item.count]),
102 );
103
104 return {
105 followers: dids.map((did) => followersMap.get(did) ?? 0),
106 following: dids.map((did) => followingMap.get(did) ?? 0),
107 posts: dids.map((did) => postsMap.get(did) ?? 0),
108 feeds: dids.map((did) => feedsMap.get(did) ?? 0),
109 };
110 }
111
112 async getSoundUsageCounts(uris: string[]) {
113 if (uris.length === 0) {
114 return { uses: [] };
115 }
116
117 // Count how many posts reference each sound URI
118 const usageAgg = await this.db.models.Post.aggregate([
119 { $match: { "sound.uri": { $in: uris } } },
120 { $group: { _id: "$sound.uri", count: { $sum: 1 } } },
121 ]);
122
123 const usageMap = new Map(
124 usageAgg.map((item: AggregationResult) => [item._id, item.count]),
125 );
126
127 return {
128 uses: uris.map((uri) => usageMap.get(uri) ?? 0),
129 };
130 }
131}