[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Cid } from "@atp/lex";
2import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
3import * as so from "../../../lex/so.ts";
4import { BackgroundQueue } from "../../background.ts";
5import { Database } from "../../db/index.ts";
6import { LikeDocument } from "../../db/models.ts";
7import { RecordProcessor } from "../processor.ts";
8
9const schema = so.sprk.feed.like.main;
10type LikeRecord = so.sprk.feed.like.Main;
11type IndexedLike = LikeDocument;
12
13const insertFn = async (
14 db: Database,
15 uri: AtUri,
16 cid: Cid,
17 obj: LikeRecord,
18 timestamp: string,
19): Promise<IndexedLike | null> => {
20 // Handle via property safely with type assertion
21 const viaObj = obj.via as { uri: string; cid: string } | undefined;
22 const via = viaObj?.uri || null;
23 const viaCid = viaObj?.cid || null;
24
25 const like = {
26 uri: uri.toString(),
27 cid: cid.toString(),
28 authorDid: uri.host,
29 subject: obj.subject.uri,
30 subjectCid: obj.subject.cid,
31 via,
32 viaCid,
33 createdAt: normalizeDatetimeAlways(obj.createdAt),
34 indexedAt: timestamp,
35 };
36
37 const insertedLike = await db.models.Like.findOneAndUpdate(
38 { uri: like.uri },
39 { $set: like },
40 { upsert: true, returnDocument: "after" },
41 );
42 return insertedLike;
43};
44
45const findDuplicate = async (
46 db: Database,
47 uri: AtUri,
48 obj: LikeRecord,
49): Promise<AtUri | null> => {
50 const found = await db.models.Like.findOne({
51 authorDid: uri.host,
52 subject: obj.subject.uri,
53 }).lean();
54 return found ? new AtUri(found.uri) : null;
55};
56
57const notifsForInsert = (obj: IndexedLike) => {
58 const subjectUri = new AtUri(obj.subject);
59 // prevent self-notifications
60 const isLikeFromSubjectUser = subjectUri.host === obj.authorDid;
61 if (isLikeFromSubjectUser) {
62 return [];
63 }
64
65 const notifs: Array<{
66 did: string;
67 reason: string;
68 author: string;
69 recordUri: string;
70 recordCid: string;
71 sortAt: string;
72 reasonSubject?: string;
73 }> = [
74 // Notification to the author of the liked record.
75 {
76 did: subjectUri.host,
77 author: obj.authorDid,
78 recordUri: obj.uri,
79 recordCid: obj.cid,
80 reason: "like" as const,
81 reasonSubject: subjectUri.toString(),
82 sortAt: obj.createdAt,
83 },
84 ];
85
86 if (obj.via) {
87 const viaUri = new AtUri(obj.via);
88 const isLikeFromViaSubjectUser = viaUri.host === obj.authorDid;
89 // prevent self-notifications
90 if (!isLikeFromViaSubjectUser) {
91 notifs.push(
92 // Notification to the reposter via whose repost the like was made.
93 {
94 did: viaUri.host,
95 author: obj.authorDid,
96 recordUri: obj.uri,
97 recordCid: obj.cid,
98 reason: "like-via-repost" as const,
99 reasonSubject: viaUri.toString(),
100 sortAt: obj.createdAt,
101 },
102 );
103 }
104 }
105
106 return notifs;
107};
108
109const deleteFn = async (
110 db: Database,
111 uri: AtUri,
112): Promise<IndexedLike | null> => {
113 const deleted = await db.models.Like.findOneAndDelete({
114 uri: uri.toString(),
115 });
116 return deleted;
117};
118
119const notifsForDelete = (
120 deleted: IndexedLike,
121 replacedBy: IndexedLike | null,
122) => {
123 const toDelete = replacedBy ? [] : [deleted.uri];
124 return { notifs: [], toDelete };
125};
126
127const updateAggregates = async (db: Database, like: IndexedLike) => {
128 const likeCount = await db.models.Like.countDocuments({
129 subject: like.subject,
130 });
131
132 const subjectUri = new AtUri(like.subject);
133
134 if (subjectUri.collection === "so.sprk.feed.generator") {
135 const existingGenerator = await db.models.Generator.findOne({
136 uri: like.subject,
137 });
138
139 if (existingGenerator) {
140 await db.models.Generator.findOneAndUpdate(
141 { uri: like.subject },
142 { $set: { likeCount } },
143 { returnDocument: "after" },
144 );
145 }
146 } else {
147 const existingPost = await db.models.Post.findOne({
148 uri: like.subject,
149 });
150
151 if (existingPost) {
152 await db.models.Post.findOneAndUpdate(
153 { uri: like.subject },
154 { $set: { likeCount } },
155 { returnDocument: "after" },
156 );
157 }
158
159 const existingReply = await db.models.Reply.findOne({
160 uri: like.subject,
161 });
162
163 if (existingReply) {
164 await db.models.Reply.findOneAndUpdate(
165 { uri: like.subject },
166 { $set: { likeCount } },
167 { returnDocument: "after" },
168 );
169 }
170
171 const existingCrosspostReply = await db.models.CrosspostReply.findOne({
172 uri: like.subject,
173 });
174
175 if (existingCrosspostReply) {
176 await db.models.CrosspostReply.findOneAndUpdate(
177 { uri: like.subject },
178 { $set: { likeCount } },
179 { returnDocument: "after" },
180 );
181 }
182 }
183};
184
185export type PluginType = RecordProcessor<typeof schema, IndexedLike>;
186
187export const makePlugin = (
188 db: Database,
189 background: BackgroundQueue,
190): PluginType => {
191 return new RecordProcessor(db, background, {
192 schema,
193 insertFn,
194 findDuplicate,
195 deleteFn,
196 notifsForInsert,
197 notifsForDelete,
198 updateAggregates,
199 });
200};
201
202export default makePlugin;