[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 202 lines 5.0 kB view raw
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;