[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { CID } from "multiformats/cid";
2import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
3import * as lex from "../../../lex/lexicons.ts";
4import * as Like from "../../../lex/types/so/sprk/feed/like.ts";
5import { BackgroundQueue } from "../../background.ts";
6import { Database } from "../../db/index.ts";
7import { LikeDocument } from "../../db/models.ts";
8import { RecordProcessor } from "../processor.ts";
9
10const lexId = lex.ids.SoSprkFeedLike;
11type IndexedLike = LikeDocument;
12
13const insertFn = async (
14 db: Database,
15 uri: AtUri,
16 cid: CID,
17 obj: Like.Record,
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, new: true },
41 );
42 return insertedLike;
43};
44
45const findDuplicate = async (
46 db: Database,
47 uri: AtUri,
48 obj: Like.Record,
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 { new: true },
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 { new: true },
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 { new: true },
168 );
169 }
170 }
171};
172
173export type PluginType = RecordProcessor<Like.Record, IndexedLike>;
174
175export const makePlugin = (
176 db: Database,
177 background: BackgroundQueue,
178): PluginType => {
179 return new RecordProcessor(db, background, {
180 lexId,
181 insertFn,
182 findDuplicate,
183 deleteFn,
184 notifsForInsert,
185 notifsForDelete,
186 updateAggregates,
187 });
188};
189
190export default makePlugin;