[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Document, Model, Schema } from "mongoose";
2
3interface RecordRef {
4 uri: string;
5 cid: string;
6}
7
8// Plugin for adding author DID population to schemas
9function addAuthor(schema: Schema) {
10 // Only add if schema has authorDid field
11 if (schema.paths.authorDid) {
12 schema.virtual("actor", {
13 ref: "Actor",
14 localField: "authorDid",
15 foreignField: "did",
16 justOne: true,
17 });
18
19 // Ensure virtual fields are serialized
20 schema.set("toJSON", { virtuals: true });
21 schema.set("toObject", { virtuals: true });
22 }
23}
24
25// Base interface for documents with authorDid
26interface AuthoredDocument extends Document {
27 uri: string;
28 cid: string;
29 createdAt: string;
30 indexedAt: string;
31 authorDid: string;
32 actor?: ActorDocument; // Virtual field for populated actor data
33}
34
35export const authoredSchema = {
36 uri: { type: String, required: true, unique: true, index: true },
37 authorDid: { type: String, required: true, index: true },
38 cid: { type: String, required: true },
39 createdAt: { type: String, required: true },
40 indexedAt: { type: String, required: true },
41};
42
43export interface MediaRef {
44 $type: string;
45 ref: { $link: string };
46}
47export interface ImageMedia extends MediaRef {
48 alt: string;
49 aspectRatio: {
50 width: number;
51 height: number;
52 };
53}
54export interface VideoMedia extends MediaRef {
55 alt: string;
56 aspectRatio: {
57 width: number;
58 height: number;
59 };
60}
61interface Label {
62 src: string;
63 uri: string;
64 cid: string;
65 val: string;
66 neg: boolean;
67}
68interface Facet {
69 index: {
70 byteStart: number;
71 byteEnd: number;
72 };
73 features: Array<{
74 $type: string;
75 uri?: string;
76 did?: string;
77 tag?: string;
78 }>;
79}
80export interface PostMedia {
81 $type: string;
82 video?: VideoMedia;
83 images?: ImageMedia[];
84}
85export interface StoryMedia {
86 $type: string;
87 video?: VideoMedia;
88 image?: ImageMedia;
89}
90export interface Caption {
91 text: string;
92 facets?: Facet[];
93}
94
95// records
96
97export interface RecordDocument extends Document {
98 uri: string;
99 cid: string;
100 did: string;
101 collectionName: string;
102 rkey: string;
103 createdAt: string;
104 indexedAt: string;
105 json: string;
106 takenDown: boolean;
107 takedownRef: string;
108 invalidReplyRoot?: boolean;
109}
110export const recordSchema = new Schema<RecordDocument>({
111 uri: { type: String, required: true, unique: true, index: true },
112 cid: { type: String, required: true },
113 did: { type: String, required: true, index: true },
114 collectionName: { type: String, required: true, index: true },
115 rkey: { type: String, required: true },
116 createdAt: { type: String, required: true },
117 indexedAt: { type: String, required: true },
118 json: { type: String, required: true },
119 takenDown: { type: Boolean, required: false },
120 takedownRef: { type: String, required: false },
121 invalidReplyRoot: { type: Boolean, required: false },
122});
123
124// duplicate records
125
126export interface DuplicateRecordDocument extends Document {
127 uri: string;
128 cid: string;
129 duplicateOf: string;
130 indexedAt: string;
131}
132export const duplicateRecordSchema = new Schema<DuplicateRecordDocument>({
133 uri: { type: String, required: true, unique: true, index: true },
134 cid: { type: String, required: true },
135 duplicateOf: { type: String, required: true, index: true },
136 indexedAt: { type: String, required: true },
137});
138
139// actor sync
140
141export interface ActorSyncDocument extends Document {
142 did: string;
143 commitCid: string;
144 repoRev: string | null;
145}
146export const actorSyncSchema = new Schema<ActorSyncDocument>({
147 did: { type: String, required: true, unique: true, index: true },
148 commitCid: { type: String, required: true },
149 repoRev: { type: String, required: false, default: null },
150});
151
152// likes
153
154export interface LikeDocument extends AuthoredDocument {
155 subject: string;
156 subjectCid: string;
157 via?: string | null;
158 viaCid?: string | null;
159}
160export const likeSchema = new Schema<LikeDocument>({
161 ...authoredSchema,
162 subject: { type: String, required: true, index: true },
163 subjectCid: { type: String, required: true },
164 via: { type: String, required: false },
165 viaCid: { type: String, required: false },
166})
167 .index({ authorDid: 1, subject: 1 }, { unique: true })
168 .index({ subject: 1, createdAt: -1 });
169
170// follows
171
172export interface FollowDocument extends AuthoredDocument {
173 subject: string;
174}
175export const followSchema = new Schema<FollowDocument>({
176 ...authoredSchema,
177 subject: { type: String, required: true, index: true },
178})
179 .index({ authorDid: 1, subject: 1 }, { unique: true })
180 .index({ subject: 1, createdAt: -1 });
181
182// blocks
183
184export interface BlockDocument extends AuthoredDocument {
185 subject: string;
186}
187
188export const blockSchema = new Schema<BlockDocument>({
189 ...authoredSchema,
190 subject: { type: String, required: true, index: true },
191})
192 .index({ authorDid: 1, subject: 1 }, { unique: true })
193 .index({ subject: 1, createdAt: -1 });
194
195// profiles
196
197export interface ProfileDocument extends AuthoredDocument {
198 displayName?: string;
199 description?: string;
200 avatar?: MediaRef;
201 banner?: MediaRef;
202 labels?: Label[];
203 pinnedPost?: RecordRef;
204 postsCount: number;
205 followersCount: number;
206 followsCount: number;
207}
208export const profileSchema = new Schema<ProfileDocument>({
209 ...authoredSchema,
210 displayName: { type: String, required: false },
211 description: { type: String, required: false },
212 avatar: { type: Object, required: false },
213 banner: { type: Object, required: false },
214 labels: { type: [Object], required: false },
215 pinnedPost: { type: Object, required: false },
216 postsCount: { type: Number, required: true, default: 0 },
217 followersCount: { type: Number, required: true, default: 0 },
218 followsCount: { type: Number, required: true, default: 0 },
219})
220 .index({ displayName: "text", description: "text" });
221
222// audio
223
224export interface AudioDocument extends AuthoredDocument {
225 sound: MediaRef;
226 origin?: RecordRef;
227 title: string;
228 details?: {
229 artist?: string;
230 title?: string;
231 };
232 labels?: Label[];
233 useCount: number;
234}
235export const audioSchema = new Schema<AudioDocument>({
236 ...authoredSchema,
237 sound: { type: Object, required: true },
238 origin: { type: Object, required: false },
239 title: { type: String, required: true },
240 details: { type: Object, required: false },
241 labels: { type: [Object], required: false },
242 useCount: { type: Number, required: true, default: 0 },
243})
244 .index({ authorDid: 1, createdAt: -1 })
245 .index({ useCount: -1, createdAt: -1 });
246
247// reposts
248
249export interface RepostDocument extends AuthoredDocument {
250 subject: string;
251 subjectCid: string;
252 via?: string | null;
253 viaCid?: string | null;
254}
255export const repostSchema = new Schema<RepostDocument>({
256 ...authoredSchema,
257 subject: { type: String, required: true },
258 subjectCid: { type: String, required: true },
259 via: { type: String, required: false },
260 viaCid: { type: String, required: false },
261})
262 .index({ subject: 1, createdAt: -1 })
263 .index({ authorDid: 1, createdAt: -1 });
264
265// posts
266
267export interface PostDocument extends AuthoredDocument {
268 caption?: Caption;
269 media?: PostMedia;
270 sound?: RecordRef;
271 langs?: string[];
272 labels?: Label[];
273 tags?: string[];
274 likeCount: number;
275 replyCount: number;
276 repostCount: number;
277}
278export const postSchema = new Schema<PostDocument>({
279 ...authoredSchema,
280 caption: {
281 type: {
282 text: { type: String, required: true },
283 facets: { type: [Object], required: false, default: [] },
284 },
285 required: false,
286 },
287 media: { type: Object, required: false },
288 sound: {
289 type: {
290 uri: { type: String, required: true },
291 cid: { type: String, required: true },
292 },
293 required: false,
294 },
295 langs: { type: [String], required: false, default: [] },
296 labels: { type: [Object], required: false, default: [] },
297 tags: { type: [String], required: false, default: [] },
298 likeCount: { type: Number, required: true, default: 0 },
299 replyCount: { type: Number, required: true, default: 0 },
300 repostCount: { type: Number, required: true, default: 0 },
301})
302 .index({ authorDid: 1, createdAt: -1 })
303 .index({ tags: 1, createdAt: -1 });
304
305// replies
306
307export interface ReplyDocument extends AuthoredDocument {
308 text?: string;
309 facets?: Facet[];
310 reply?: {
311 root: RecordRef;
312 parent: RecordRef;
313 };
314 media?: ImageMedia;
315 langs?: string[];
316 labels?: Label[];
317 likeCount: number;
318 replyCount: number;
319}
320export const replySchema = new Schema<ReplyDocument>({
321 ...authoredSchema,
322 text: { type: String, required: false },
323 facets: { type: [Object], required: false, default: [] },
324 reply: {
325 type: {
326 root: {
327 uri: { type: String, required: true },
328 cid: { type: String, required: true },
329 },
330 parent: {
331 uri: { type: String, required: true },
332 cid: { type: String, required: true },
333 },
334 },
335 required: false,
336 },
337 media: { type: Object, required: false },
338 langs: { type: [String], required: false, default: [] },
339 labels: { type: [Object], required: false, default: [] },
340 likeCount: { type: Number, required: true, default: 0 },
341 replyCount: { type: Number, required: true, default: 0 },
342})
343 .index({ reply: 1, createdAt: -1 });
344
345// stories
346
347export interface StoryDocument extends AuthoredDocument {
348 media: StoryMedia;
349 sound?: RecordRef;
350 labels?: Label[];
351}
352export const storySchema = new Schema<StoryDocument>({
353 ...authoredSchema,
354 media: { type: Object, required: true },
355 sound: {
356 type: {
357 uri: { type: String, required: true },
358 cid: { type: String, required: true },
359 },
360 required: false,
361 },
362 labels: { type: [Object], required: false, default: [] },
363})
364 .index({ authorDid: 1, createdAt: -1 });
365
366// generators
367
368export interface GeneratorDocument extends AuthoredDocument {
369 displayName: string;
370 description?: string;
371 descriptionFacets?: Facet[];
372 avatar?: MediaRef;
373 acceptsInteractions?: boolean;
374 labels?: Label[];
375 likeCount: number;
376}
377export const generatorSchema = new Schema<GeneratorDocument>({
378 ...authoredSchema,
379 displayName: { type: String, required: true },
380 description: { type: String, required: false },
381 descriptionFacets: { type: [Object], required: false },
382 avatar: { type: Object, required: false },
383 acceptsInteractions: { type: Boolean, required: false },
384 labels: { type: [Object], required: false },
385 likeCount: { type: Number, required: false, default: 0 },
386})
387 .index({ authorDid: 1, createdAt: -1 });
388
389// labelers
390
391export interface LabelerDocument extends AuthoredDocument {}
392
393export const labelerSchema = new Schema<LabelerDocument>({
394 ...authoredSchema,
395})
396 .index({ authorDid: 1, createdAt: -1 });
397
398// labels
399
400export interface LabelDocument extends Document {
401 src: string;
402 uri: string;
403 cid: string;
404 val: string;
405 neg: boolean;
406 cts: string;
407 exp: string | null;
408}
409
410export const labelSchema = new Schema<LabelDocument>({
411 src: { type: String, required: true, index: true },
412 uri: { type: String, required: true, index: true },
413 cid: { type: String, required: true },
414 val: { type: String, required: true, index: true },
415 neg: { type: Boolean, required: true },
416 cts: { type: String, required: true },
417 exp: { type: String, required: false, default: null },
418})
419 .index({ uri: 1, src: 1, val: 1 }, { unique: true })
420 .index({ src: 1, cts: -1 });
421
422// takedowns
423
424export interface TakedownDocument extends Document {
425 targetUri: string;
426 targetCid: string;
427 reason: string;
428 takenDownBy: string;
429 takenDownAt: string;
430 ref: string | null;
431 applied: boolean;
432}
433export const takedownSchema = new Schema<TakedownDocument>({
434 targetUri: { type: String, required: true, unique: true, index: true },
435 targetCid: { type: String, required: true },
436 reason: { type: String, required: true },
437 takenDownBy: { type: String, required: true },
438 takenDownAt: { type: String, required: true },
439 ref: { type: String, required: false },
440 applied: { type: Boolean, required: true, default: false },
441});
442
443// repo takedowns
444
445export interface RepoTakedownDocument extends Document {
446 did: string;
447 reason: string;
448 takenDownBy: string;
449 takenDownAt: string;
450 ref: string | null;
451 applied: boolean;
452}
453export const repoTakedownSchema = new Schema<RepoTakedownDocument>({
454 did: { type: String, required: true, unique: true, index: true },
455 reason: { type: String, required: true },
456 takenDownBy: { type: String, required: true },
457 takenDownAt: { type: String, required: true },
458 ref: { type: String, required: false, default: null },
459 applied: { type: Boolean, required: true, default: false },
460});
461
462// blobs takedowns
463
464export interface BlobTakedownDocument extends Document {
465 did: string;
466 cid: string;
467 reason: string;
468 takenDownBy: string;
469 takenDownAt: string;
470 ref: string | null;
471 applied: boolean;
472}
473export const blobTakedownSchema = new Schema<BlobTakedownDocument>({
474 did: { type: String, required: true, index: true },
475 cid: { type: String, required: true, index: true },
476 reason: { type: String, required: true },
477 takenDownBy: { type: String, required: true },
478 takenDownAt: { type: String, required: true },
479 ref: { type: String, required: false, default: null },
480 applied: { type: Boolean, required: true, default: false },
481})
482 .index({ did: 1, cid: 1 }, { unique: true });
483
484// actors
485
486export interface ActorDocument extends Document {
487 did: string;
488 handle: string | null;
489 indexedAt: string;
490 takedownRef: string | null;
491 upstreamStatus: string | null;
492 keys: string[];
493 services: string;
494}
495export const actorSchema = new Schema<ActorDocument>({
496 did: { type: String, required: true, unique: true, index: true },
497 handle: { type: String, required: false, index: true },
498 indexedAt: { type: String, required: true },
499 takedownRef: { type: String, required: false },
500 upstreamStatus: { type: String, required: false },
501 keys: { type: [String], required: true },
502 services: { type: String, required: true },
503});
504
505// preferences
506
507export interface PreferenceDocument extends Document {
508 userDid: string;
509 contentLabelPrefs?: Array<{
510 labelerDid?: string;
511 label: string;
512 visibility: string;
513 }>;
514 savedFeeds?: Array<{
515 id: string;
516 type: string;
517 value: string;
518 pinned: boolean;
519 }>;
520 personalDetailsPref?: {
521 birthDate?: string;
522 };
523 feedViewPrefs?: Array<{
524 feed: string;
525 hideReplies?: boolean;
526 hideRepliesByUnfollowed: boolean;
527 hideRepliesByLikeCount?: number;
528 hideReposts?: boolean;
529 hideQuotePosts?: boolean;
530 }>;
531 threadViewPref?: {
532 sort?: string;
533 };
534 interestsPref?: {
535 tags: string[];
536 };
537 mutedWordsPref?: {
538 items: Array<{
539 id?: string;
540 value: string;
541 targets: string[];
542 actorTarget: string;
543 expiresAt?: string;
544 }>;
545 };
546 hiddenPostsPref?: {
547 items: string[];
548 };
549 labelersPref?: {
550 labelers: Array<{
551 did: string;
552 }>;
553 };
554 postInteractionSettingsPref?: {
555 threadgateAllowRules?: Array<{
556 $type: string;
557 [key: string]: unknown;
558 }>;
559 };
560 createdAt: string;
561 updatedAt: string;
562}
563export const preferenceSchema = new Schema<PreferenceDocument>({
564 userDid: { type: String, required: true, unique: true, index: true },
565 contentLabelPrefs: { type: [Object], required: false },
566 savedFeeds: { type: [Object], required: false },
567 personalDetailsPref: { type: Object, required: false },
568 feedViewPrefs: { type: [Object], required: false },
569 threadViewPref: { type: Object, required: false },
570 interestsPref: { type: Object, required: false },
571 mutedWordsPref: { type: Object, required: false },
572 hiddenPostsPref: { type: Object, required: false },
573 labelersPref: { type: Object, required: false },
574 postInteractionSettingsPref: { type: Object, required: false },
575 createdAt: { type: String, required: true },
576 updatedAt: { type: String, required: true },
577});
578
579// cursor state
580
581export interface CursorStateDocument extends Document {
582 identifier: string; // To ensure a single document, e.g., 'last_processed_cursor'
583 cursorValue: number;
584 updatedAt: Date;
585}
586export const cursorStateSchema = new Schema<CursorStateDocument>({
587 identifier: { type: String, required: true, unique: true, index: true },
588 cursorValue: { type: Number, required: true },
589 updatedAt: { type: Date, default: Date.now },
590});
591
592// Apply plugin to schemas that extend AuthoredDocument
593([
594 profileSchema,
595 likeSchema,
596 postSchema,
597 replySchema,
598 repostSchema,
599 followSchema,
600 blockSchema,
601 generatorSchema,
602 audioSchema,
603 storySchema,
604 labelerSchema,
605] as Schema[]).forEach((s) => s.plugin(addAuthor));
606
607export interface DatabaseModels {
608 Record: Model<RecordDocument>;
609 DuplicateRecord: Model<DuplicateRecordDocument>;
610 Like: Model<LikeDocument>;
611 Post: Model<PostDocument>;
612 Reply: Model<ReplyDocument>;
613 Story: Model<StoryDocument>;
614 Follow: Model<FollowDocument>;
615 Block: Model<BlockDocument>;
616 Profile: Model<ProfileDocument>;
617 Audio: Model<AudioDocument>;
618 Repost: Model<RepostDocument>;
619 Generator: Model<GeneratorDocument>;
620 Labeler: Model<LabelerDocument>;
621 Label: Model<LabelDocument>;
622 Takedown: Model<TakedownDocument>;
623 RepoTakedown: Model<RepoTakedownDocument>;
624 BlobTakedown: Model<BlobTakedownDocument>;
625 Actor: Model<ActorDocument>;
626 ActorSync: Model<ActorSyncDocument>;
627 Preference: Model<PreferenceDocument>;
628 CursorState: Model<CursorStateDocument>;
629}