[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 eb947da8a3a8a7d485ff132b3ff0db4a8baaac19 594 lines 16 kB view raw
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// takedowns 390 391export interface TakedownDocument extends Document { 392 targetUri: string; 393 targetCid: string; 394 reason: string; 395 takenDownBy: string; 396 takenDownAt: string; 397 ref: string | null; 398 applied: boolean; 399} 400export const takedownSchema = new Schema<TakedownDocument>({ 401 targetUri: { type: String, required: true, unique: true, index: true }, 402 targetCid: { type: String, required: true }, 403 reason: { type: String, required: true }, 404 takenDownBy: { type: String, required: true }, 405 takenDownAt: { type: String, required: true }, 406 ref: { type: String, required: false }, 407 applied: { type: Boolean, required: true, default: false }, 408}); 409 410// repo takedowns 411 412export interface RepoTakedownDocument extends Document { 413 did: string; 414 reason: string; 415 takenDownBy: string; 416 takenDownAt: string; 417 ref: string | null; 418 applied: boolean; 419} 420export const repoTakedownSchema = new Schema<RepoTakedownDocument>({ 421 did: { type: String, required: true, unique: true, index: true }, 422 reason: { type: String, required: true }, 423 takenDownBy: { type: String, required: true }, 424 takenDownAt: { type: String, required: true }, 425 ref: { type: String, required: false, default: null }, 426 applied: { type: Boolean, required: true, default: false }, 427}); 428 429// blobs takedowns 430 431export interface BlobTakedownDocument extends Document { 432 did: string; 433 cid: string; 434 reason: string; 435 takenDownBy: string; 436 takenDownAt: string; 437 ref: string | null; 438 applied: boolean; 439} 440export const blobTakedownSchema = new Schema<BlobTakedownDocument>({ 441 did: { type: String, required: true, index: true }, 442 cid: { type: String, required: true, index: true }, 443 reason: { type: String, required: true }, 444 takenDownBy: { type: String, required: true }, 445 takenDownAt: { type: String, required: true }, 446 ref: { type: String, required: false, default: null }, 447 applied: { type: Boolean, required: true, default: false }, 448}) 449 .index({ did: 1, cid: 1 }, { unique: true }); 450 451// actors 452 453export interface ActorDocument extends Document { 454 did: string; 455 handle: string | null; 456 indexedAt: string; 457 takedownRef: string | null; 458 upstreamStatus: string | null; 459 keys: string[]; 460 services: string; 461} 462export const actorSchema = new Schema<ActorDocument>({ 463 did: { type: String, required: true, unique: true, index: true }, 464 handle: { type: String, required: false, index: true }, 465 indexedAt: { type: String, required: true }, 466 takedownRef: { type: String, required: false }, 467 upstreamStatus: { type: String, required: false }, 468 keys: { type: [String], required: true }, 469 services: { type: String, required: true }, 470}); 471 472// preferences 473 474export interface PreferenceDocument extends Document { 475 userDid: string; 476 contentLabelPrefs?: Array<{ 477 labelerDid?: string; 478 label: string; 479 visibility: string; 480 }>; 481 savedFeeds?: Array<{ 482 id: string; 483 type: string; 484 value: string; 485 pinned: boolean; 486 }>; 487 personalDetailsPref?: { 488 birthDate?: string; 489 }; 490 feedViewPrefs?: Array<{ 491 feed: string; 492 hideReplies?: boolean; 493 hideRepliesByUnfollowed: boolean; 494 hideRepliesByLikeCount?: number; 495 hideRepliesByLookCount?: number; 496 hideReposts?: boolean; 497 hideQuotePosts?: boolean; 498 }>; 499 threadViewPref?: { 500 sort?: string; 501 }; 502 interestsPref?: { 503 tags: string[]; 504 }; 505 mutedWordsPref?: { 506 items: Array<{ 507 id?: string; 508 value: string; 509 targets: string[]; 510 actorTarget: string; 511 expiresAt?: string; 512 }>; 513 }; 514 hiddenPostsPref?: { 515 items: string[]; 516 }; 517 labelersPref?: { 518 labelers: Array<{ 519 did: string; 520 }>; 521 }; 522 postInteractionSettingsPref?: { 523 threadgateAllowRules?: Array<{ 524 $type: string; 525 [key: string]: unknown; 526 }>; 527 }; 528 createdAt: string; 529 updatedAt: string; 530} 531export const preferenceSchema = new Schema<PreferenceDocument>({ 532 userDid: { type: String, required: true, unique: true, index: true }, 533 contentLabelPrefs: { type: [Object], required: false }, 534 savedFeeds: { type: [Object], required: false }, 535 personalDetailsPref: { type: Object, required: false }, 536 feedViewPrefs: { type: [Object], required: false }, 537 threadViewPref: { type: Object, required: false }, 538 interestsPref: { type: Object, required: false }, 539 mutedWordsPref: { type: Object, required: false }, 540 hiddenPostsPref: { type: Object, required: false }, 541 labelersPref: { type: Object, required: false }, 542 postInteractionSettingsPref: { type: Object, required: false }, 543 createdAt: { type: String, required: true }, 544 updatedAt: { type: String, required: true }, 545}); 546 547// cursor state 548 549export interface CursorStateDocument extends Document { 550 identifier: string; // To ensure a single document, e.g., 'last_processed_cursor' 551 cursorValue: number; 552 updatedAt: Date; 553} 554export const cursorStateSchema = new Schema<CursorStateDocument>({ 555 identifier: { type: String, required: true, unique: true, index: true }, 556 cursorValue: { type: Number, required: true }, 557 updatedAt: { type: Date, default: Date.now }, 558}); 559 560// Apply plugin to schemas that extend AuthoredDocument 561([ 562 profileSchema, 563 likeSchema, 564 postSchema, 565 replySchema, 566 repostSchema, 567 followSchema, 568 blockSchema, 569 generatorSchema, 570 audioSchema, 571 storySchema, 572] as Schema[]).forEach((s) => s.plugin(addAuthor)); 573 574export interface DatabaseModels { 575 Record: Model<RecordDocument>; 576 DuplicateRecord: Model<DuplicateRecordDocument>; 577 Like: Model<LikeDocument>; 578 Post: Model<PostDocument>; 579 Reply: Model<ReplyDocument>; 580 Story: Model<StoryDocument>; 581 Follow: Model<FollowDocument>; 582 Block: Model<BlockDocument>; 583 Profile: Model<ProfileDocument>; 584 Audio: Model<AudioDocument>; 585 Repost: Model<RepostDocument>; 586 Generator: Model<GeneratorDocument>; 587 Takedown: Model<TakedownDocument>; 588 RepoTakedown: Model<RepoTakedownDocument>; 589 BlobTakedown: Model<BlobTakedownDocument>; 590 Actor: Model<ActorDocument>; 591 ActorSync: Model<ActorSyncDocument>; 592 Preference: Model<PreferenceDocument>; 593 CursorState: Model<CursorStateDocument>; 594}