[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 3b73895e29748ca524bbe040b656ddb4e167104b 741 lines 21 kB view raw
1import { 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 { 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 mimeType?: string; 47 size?: number; 48} 49export interface ImageMedia extends MediaRef { 50 alt: string; 51 aspectRatio: { 52 width: number; 53 height: number; 54 }; 55} 56export interface VideoMedia extends MediaRef { 57 alt: string; 58 aspectRatio: { 59 width: number; 60 height: number; 61 }; 62} 63interface Label { 64 src: string; 65 uri: string; 66 cid: string; 67 val: string; 68 neg: boolean; 69} 70interface Facet { 71 index: { 72 byteStart: number; 73 byteEnd: number; 74 }; 75 features: Array<{ 76 $type: string; 77 uri?: string; 78 did?: string; 79 tag?: string; 80 }>; 81} 82export interface PostMedia { 83 $type: string; 84 video?: VideoMedia; 85 images?: ImageMedia[]; 86} 87export interface StoryMedia { 88 $type: string; 89 video?: VideoMedia; 90 image?: ImageMedia; 91 alt?: string; 92 aspectRatio?: { 93 width: number; 94 height: number; 95 }; 96} 97export interface Caption { 98 text: string; 99 facets?: Facet[]; 100} 101 102// records 103 104export interface RecordDocument { 105 uri: string; 106 cid: string; 107 did: string; 108 collectionName: string; 109 rkey: string; 110 createdAt: string; 111 indexedAt: string; 112 json: string; 113 takenDown: boolean; 114 takedownRef: string; 115 invalidReplyRoot?: boolean; 116} 117export const recordSchema = new Schema<RecordDocument>({ 118 uri: { type: String, required: true, unique: true, index: true }, 119 cid: { type: String, required: true }, 120 did: { type: String, required: true, index: true }, 121 collectionName: { type: String, required: true, index: true }, 122 rkey: { type: String, required: true }, 123 createdAt: { type: String, required: true }, 124 indexedAt: { type: String, required: true }, 125 json: { type: String, required: true }, 126 takenDown: { type: Boolean, required: false }, 127 takedownRef: { type: String, required: false }, 128 invalidReplyRoot: { type: Boolean, required: false }, 129}); 130 131// duplicate records 132 133export interface DuplicateRecordDocument { 134 uri: string; 135 cid: string; 136 duplicateOf: string; 137 indexedAt: string; 138} 139export const duplicateRecordSchema = new Schema<DuplicateRecordDocument>({ 140 uri: { type: String, required: true, unique: true, index: true }, 141 cid: { type: String, required: true }, 142 duplicateOf: { type: String, required: true, index: true }, 143 indexedAt: { type: String, required: true }, 144}); 145 146// actor sync 147 148export interface ActorSyncDocument { 149 did: string; 150 commitCid: string; 151 repoRev: string | null; 152} 153export const actorSyncSchema = new Schema<ActorSyncDocument>({ 154 did: { type: String, required: true, unique: true, index: true }, 155 commitCid: { type: String, required: true }, 156 repoRev: { type: String, required: false, default: null }, 157}); 158 159// likes 160 161export interface LikeDocument extends AuthoredDocument { 162 subject: string; 163 subjectCid: string; 164 via?: string | null; 165 viaCid?: string | null; 166} 167export const likeSchema = new Schema<LikeDocument>({ 168 ...authoredSchema, 169 subject: { type: String, required: true, index: true }, 170 subjectCid: { type: String, required: true }, 171 via: { type: String, required: false }, 172 viaCid: { type: String, required: false }, 173}) 174 .index({ authorDid: 1, subject: 1 }, { unique: true }) 175 .index({ subject: 1, createdAt: -1 }) 176 .index({ subject: 1, authorDid: 1 }); 177 178// follows 179 180export interface FollowDocument extends AuthoredDocument { 181 subject: string; 182} 183export const followSchema = new Schema<FollowDocument>({ 184 ...authoredSchema, 185 subject: { type: String, required: true, index: true }, 186}) 187 .index({ authorDid: 1, subject: 1 }, { unique: true }) 188 .index({ subject: 1, createdAt: -1 }) 189 .index({ subject: 1, authorDid: 1 }); 190 191// blocks 192 193export interface BlockDocument extends AuthoredDocument { 194 subject: string; 195} 196 197export const blockSchema = new Schema<BlockDocument>({ 198 ...authoredSchema, 199 subject: { type: String, required: true, index: true }, 200}) 201 .index({ authorDid: 1, subject: 1 }, { unique: true }) 202 .index({ subject: 1, createdAt: -1 }) 203 .index({ subject: 1, authorDid: 1 }); 204 205// profiles 206 207export interface ProfileDocument extends AuthoredDocument { 208 displayName?: string; 209 description?: string; 210 avatar?: MediaRef; 211 banner?: MediaRef; 212 labels?: Label[]; 213 pinnedPost?: RecordRef; 214 postsCount: number; 215 followersCount: number; 216 followsCount: number; 217} 218export const profileSchema = new Schema<ProfileDocument>({ 219 ...authoredSchema, 220 displayName: { type: String, required: false }, 221 description: { type: String, required: false }, 222 avatar: { type: Object, required: false }, 223 banner: { type: Object, required: false }, 224 labels: { type: [Object], required: false }, 225 pinnedPost: { type: Object, required: false }, 226 postsCount: { type: Number, required: true, default: 0 }, 227 followersCount: { type: Number, required: true, default: 0 }, 228 followsCount: { type: Number, required: true, default: 0 }, 229}) 230 .index({ displayName: "text", description: "text" }); 231 232// audio 233 234export interface AudioDocument extends AuthoredDocument { 235 sound: MediaRef; 236 origin?: RecordRef; 237 title: string; 238 details?: { 239 artist?: string; 240 title?: string; 241 }; 242 labels?: Label[]; 243 useCount: number; 244} 245export const audioSchema = new Schema<AudioDocument>({ 246 ...authoredSchema, 247 sound: { type: Object, required: true }, 248 origin: { type: Object, required: false }, 249 title: { type: String, required: true }, 250 details: { type: Object, required: false }, 251 labels: { type: [Object], required: false }, 252 useCount: { type: Number, required: true, default: 0 }, 253}) 254 .index({ authorDid: 1, createdAt: -1 }) 255 .index({ useCount: -1, createdAt: -1 }); 256 257// reposts 258 259export interface RepostDocument extends AuthoredDocument { 260 subject: string; 261 subjectCid: string; 262 via?: string | null; 263 viaCid?: string | null; 264} 265export const repostSchema = new Schema<RepostDocument>({ 266 ...authoredSchema, 267 subject: { type: String, required: true }, 268 subjectCid: { type: String, required: true }, 269 via: { type: String, required: false }, 270 viaCid: { type: String, required: false }, 271}) 272 .index({ subject: 1, createdAt: -1 }) 273 .index({ authorDid: 1, createdAt: -1 }) 274 .index({ subject: 1, authorDid: 1 }); 275 276// posts 277 278export interface PostDocument extends AuthoredDocument { 279 caption?: Caption; 280 media?: PostMedia; 281 sound?: RecordRef; 282 langs?: string[]; 283 labels?: Label[]; 284 tags?: string[]; 285 crossposts?: RecordRef[]; 286 likeCount: number; 287 replyCount: number; 288 repostCount: number; 289} 290export const postSchema = new Schema<PostDocument>({ 291 ...authoredSchema, 292 caption: { 293 type: { 294 text: { type: String, required: true }, 295 facets: { type: [Object], required: false, default: [] }, 296 }, 297 required: false, 298 }, 299 media: { type: Object, required: false }, 300 sound: { 301 type: { 302 uri: { type: String, required: true }, 303 cid: { type: String, required: true }, 304 }, 305 required: false, 306 }, 307 langs: { type: [String], required: false, default: [] }, 308 labels: { type: [Object], required: false, default: [] }, 309 tags: { type: [String], required: false, default: [] }, 310 crossposts: { type: [Object], required: false, default: [] }, 311 likeCount: { type: Number, required: true, default: 0 }, 312 replyCount: { type: Number, required: true, default: 0 }, 313 repostCount: { type: Number, required: true, default: 0 }, 314}) 315 .index({ authorDid: 1, createdAt: -1 }) 316 .index({ tags: 1, createdAt: -1 }); 317 318// replies 319 320export interface ReplyDocument extends AuthoredDocument { 321 text?: string; 322 facets?: Facet[]; 323 reply?: { 324 root: RecordRef; 325 parent: RecordRef; 326 }; 327 media?: ImageMedia | { images?: ImageMedia[]; [key: string]: unknown }; 328 langs?: string[]; 329 labels?: Label[]; 330 invalidReplyRoot?: boolean; 331 likeCount: number; 332 replyCount: number; 333} 334export const replySchema = new Schema<ReplyDocument>({ 335 ...authoredSchema, 336 text: { type: String, required: false }, 337 facets: { type: [Object], required: false, default: [] }, 338 reply: { 339 type: { 340 root: { 341 uri: { type: String, required: true }, 342 cid: { type: String, required: true }, 343 }, 344 parent: { 345 uri: { type: String, required: true }, 346 cid: { type: String, required: true }, 347 }, 348 }, 349 required: false, 350 }, 351 media: { type: Object, required: false }, 352 langs: { type: [String], required: false, default: [] }, 353 labels: { type: [Object], required: false, default: [] }, 354 invalidReplyRoot: { type: Boolean, required: false }, 355 likeCount: { type: Number, required: true, default: 0 }, 356 replyCount: { type: Number, required: true, default: 0 }, 357}) 358 .index({ reply: 1, createdAt: -1 }) 359 .index({ "reply.parent.uri": 1, authorDid: 1 }) 360 .index({ "reply.root.uri": 1, createdAt: -1 }); 361 362// crosspost replies 363 364export interface CrosspostReplyDocument extends AuthoredDocument { 365 text?: string; 366 facets?: Facet[]; 367 reply?: { 368 root: RecordRef; 369 parent: RecordRef; 370 }; 371 langs?: string[]; 372 labels?: Label[]; 373 tags?: string[]; 374 invalidReplyRoot?: boolean; 375 likeCount: number; 376 replyCount: number; 377} 378export const crosspostReplySchema = new Schema<CrosspostReplyDocument>({ 379 ...authoredSchema, 380 text: { type: String, required: false }, 381 facets: { type: [Object], required: false, default: [] }, 382 reply: { 383 type: { 384 root: { 385 uri: { type: String, required: true }, 386 cid: { type: String, required: true }, 387 }, 388 parent: { 389 uri: { type: String, required: true }, 390 cid: { type: String, required: true }, 391 }, 392 }, 393 required: false, 394 }, 395 langs: { type: [String], required: false, default: [] }, 396 labels: { type: [Object], required: false, default: [] }, 397 tags: { type: [String], required: false, default: [] }, 398 invalidReplyRoot: { type: Boolean, required: false }, 399 likeCount: { type: Number, required: true, default: 0 }, 400 replyCount: { type: Number, required: true, default: 0 }, 401}) 402 .index({ reply: 1, createdAt: -1 }) 403 .index({ "reply.parent.uri": 1, authorDid: 1 }) 404 .index({ "reply.root.uri": 1, createdAt: -1 }); 405 406// stories 407 408export interface StoryDocument extends AuthoredDocument { 409 media: StoryMedia; 410 sound?: RecordRef; 411 labels?: Label[]; 412} 413export const storySchema = new Schema<StoryDocument>({ 414 ...authoredSchema, 415 media: { type: Object, required: true }, 416 sound: { 417 type: { 418 uri: { type: String, required: true }, 419 cid: { type: String, required: true }, 420 }, 421 required: false, 422 }, 423 labels: { type: [Object], required: false, default: [] }, 424}) 425 .index({ authorDid: 1, createdAt: -1 }); 426 427// generators 428 429export interface GeneratorDocument extends AuthoredDocument { 430 displayName: string; 431 description?: string; 432 descriptionFacets?: Facet[]; 433 avatar?: MediaRef; 434 acceptsInteractions?: boolean; 435 labels?: Label[]; 436 likeCount: number; 437} 438export const generatorSchema = new Schema<GeneratorDocument>({ 439 ...authoredSchema, 440 displayName: { type: String, required: true }, 441 description: { type: String, required: false }, 442 descriptionFacets: { type: [Object], required: false }, 443 avatar: { type: Object, required: false }, 444 acceptsInteractions: { type: Boolean, required: false }, 445 labels: { type: [Object], required: false }, 446 likeCount: { type: Number, required: false, default: 0 }, 447}) 448 .index({ authorDid: 1, createdAt: -1 }); 449 450// labelers 451 452export interface LabelerDocument extends AuthoredDocument {} 453 454export const labelerSchema = new Schema<LabelerDocument>({ 455 ...authoredSchema, 456}) 457 .index({ authorDid: 1, createdAt: -1 }); 458 459// labels 460 461export interface LabelDocument { 462 src: string; 463 uri: string; 464 cid: string; 465 val: string; 466 neg: boolean; 467 cts: string; 468 exp: string | null; 469} 470 471export const labelSchema = new Schema<LabelDocument>({ 472 src: { type: String, required: true, index: true }, 473 uri: { type: String, required: true, index: true }, 474 cid: { type: String, required: true }, 475 val: { type: String, required: true, index: true }, 476 neg: { type: Boolean, required: true }, 477 cts: { type: String, required: true }, 478 exp: { type: String, required: false, default: null }, 479}) 480 .index({ uri: 1, src: 1, val: 1 }, { unique: true }) 481 .index({ src: 1, cts: -1 }); 482 483// takedowns 484 485export interface TakedownDocument { 486 targetUri: string; 487 targetCid: string; 488 reason: string; 489 takenDownBy: string; 490 takenDownAt: string; 491 ref: string | null; 492 applied: boolean; 493} 494export const takedownSchema = new Schema<TakedownDocument>({ 495 targetUri: { type: String, required: true, unique: true, index: true }, 496 targetCid: { type: String, required: true }, 497 reason: { type: String, required: true }, 498 takenDownBy: { type: String, required: true }, 499 takenDownAt: { type: String, required: true }, 500 ref: { type: String, required: false }, 501 applied: { type: Boolean, required: true, default: false }, 502}); 503 504// repo takedowns 505 506export interface RepoTakedownDocument { 507 did: string; 508 reason: string; 509 takenDownBy: string; 510 takenDownAt: string; 511 ref: string | null; 512 applied: boolean; 513} 514export const repoTakedownSchema = new Schema<RepoTakedownDocument>({ 515 did: { type: String, required: true, unique: true, index: true }, 516 reason: { type: String, required: true }, 517 takenDownBy: { type: String, required: true }, 518 takenDownAt: { type: String, required: true }, 519 ref: { type: String, required: false, default: null }, 520 applied: { type: Boolean, required: true, default: false }, 521}); 522 523// blobs takedowns 524 525export interface BlobTakedownDocument { 526 did: string; 527 cid: string; 528 reason: string; 529 takenDownBy: string; 530 takenDownAt: string; 531 ref: string | null; 532 applied: boolean; 533} 534export const blobTakedownSchema = new Schema<BlobTakedownDocument>({ 535 did: { type: String, required: true, index: true }, 536 cid: { type: String, required: true, index: true }, 537 reason: { type: String, required: true }, 538 takenDownBy: { type: String, required: true }, 539 takenDownAt: { type: String, required: true }, 540 ref: { type: String, required: false, default: null }, 541 applied: { type: Boolean, required: true, default: false }, 542}) 543 .index({ did: 1, cid: 1 }, { unique: true }); 544 545// actors 546 547export interface ActorDocument { 548 did: string; 549 handle: string | null; 550 indexedAt: string; 551 takedownRef: string | null; 552 upstreamStatus: string | null; 553 keys: string[]; 554 services: string; 555 lastSeenNotifs: string | null; 556} 557export const actorSchema = new Schema<ActorDocument>({ 558 did: { type: String, required: true, unique: true, index: true }, 559 handle: { type: String, required: false, index: true }, 560 indexedAt: { type: String, required: true }, 561 takedownRef: { type: String, required: false }, 562 upstreamStatus: { type: String, required: false }, 563 keys: { type: [String], required: true }, 564 services: { type: String, required: true }, 565 lastSeenNotifs: { type: String, required: false, default: null }, 566}); 567 568// preferences 569 570export interface PreferenceDocument { 571 userDid: string; 572 contentLabelPrefs?: Array<{ 573 labelerDid?: string; 574 label: string; 575 visibility: string; 576 }>; 577 savedFeeds?: Array<{ 578 id: string; 579 type: string; 580 value: string; 581 pinned: boolean; 582 }>; 583 personalDetailsPref?: { 584 birthDate?: string; 585 }; 586 feedViewPrefs?: Array<{ 587 feed: string; 588 hideReplies?: boolean; 589 hideRepliesByUnfollowed: boolean; 590 hideRepliesByLikeCount?: number; 591 hideReposts?: boolean; 592 hideQuotePosts?: boolean; 593 }>; 594 threadViewPref?: { 595 sort?: string; 596 }; 597 interestsPref?: { 598 tags: string[]; 599 }; 600 mutedWordsPref?: { 601 items: Array<{ 602 id?: string; 603 value: string; 604 targets: string[]; 605 actorTarget: string; 606 expiresAt?: string; 607 }>; 608 }; 609 hiddenPostsPref?: { 610 items: string[]; 611 }; 612 labelersPref?: { 613 labelers: Array<{ 614 did: string; 615 }>; 616 }; 617 postInteractionSettingsPref?: { 618 threadgateAllowRules?: Array<{ 619 $type: string; 620 [key: string]: unknown; 621 }>; 622 }; 623 createdAt: string; 624 updatedAt: string; 625} 626export const preferenceSchema = new Schema<PreferenceDocument>({ 627 userDid: { type: String, required: true, unique: true, index: true }, 628 contentLabelPrefs: { type: [Object], required: false }, 629 savedFeeds: { type: [Object], required: false }, 630 personalDetailsPref: { type: Object, required: false }, 631 feedViewPrefs: { type: [Object], required: false }, 632 threadViewPref: { type: Object, required: false }, 633 interestsPref: { type: Object, required: false }, 634 mutedWordsPref: { type: Object, required: false }, 635 hiddenPostsPref: { type: Object, required: false }, 636 labelersPref: { type: Object, required: false }, 637 postInteractionSettingsPref: { type: Object, required: false }, 638 createdAt: { type: String, required: true }, 639 updatedAt: { type: String, required: true }, 640}); 641 642// cursor state 643 644export interface CursorStateDocument { 645 identifier: string; // To ensure a single document, e.g., 'last_processed_cursor' 646 cursorValue: number; 647 updatedAt: Date; 648} 649export const cursorStateSchema = new Schema<CursorStateDocument>({ 650 identifier: { type: String, required: true, unique: true, index: true }, 651 cursorValue: { type: Number, required: true }, 652 updatedAt: { type: Date, default: Date.now }, 653}); 654 655// notifications 656 657export interface NotificationDocument { 658 did: string; 659 recordUri: string; 660 recordCid: string; 661 author: string; 662 reason: string; 663 reasonSubject: string | null; 664 sortAt: string; 665} 666export const notificationSchema = new Schema<NotificationDocument>({ 667 did: { type: String, required: true, index: true }, 668 recordUri: { type: String, required: true, index: true }, 669 recordCid: { type: String, required: true }, 670 author: { type: String, required: true, index: true }, 671 reason: { type: String, required: true }, 672 reasonSubject: { type: String, required: false, default: null }, 673 sortAt: { type: String, required: true, index: true }, 674}) 675 .index({ did: 1, sortAt: -1 }) 676 .index({ did: 1, reason: 1, sortAt: -1 }); 677 678// push tokens 679 680export interface PushTokenDocument { 681 did: string; 682 token: string; 683 platform: "ios" | "android" | "web"; 684 appId: string; 685 serviceDid: string; 686 createdAt: string; 687 updatedAt: string; 688} 689export const pushTokenSchema = new Schema<PushTokenDocument>({ 690 did: { type: String, required: true, index: true }, 691 token: { type: String, required: true }, 692 platform: { type: String, required: true, enum: ["ios", "android", "web"] }, 693 appId: { type: String, required: true }, 694 serviceDid: { type: String, required: true }, 695 createdAt: { type: String, required: true }, 696 updatedAt: { type: String, required: true }, 697}) 698 .index({ did: 1, token: 1, platform: 1, appId: 1 }, { unique: true }); 699 700// Apply plugin to schemas that extend AuthoredDocument 701([ 702 profileSchema, 703 likeSchema, 704 postSchema, 705 replySchema, 706 crosspostReplySchema, 707 repostSchema, 708 followSchema, 709 blockSchema, 710 generatorSchema, 711 audioSchema, 712 storySchema, 713 labelerSchema, 714] as Schema[]).forEach((s) => s.plugin(addAuthor)); 715 716export interface DatabaseModels { 717 Record: Model<RecordDocument>; 718 DuplicateRecord: Model<DuplicateRecordDocument>; 719 Like: Model<LikeDocument>; 720 Post: Model<PostDocument>; 721 Reply: Model<ReplyDocument>; 722 CrosspostReply: Model<CrosspostReplyDocument>; 723 Story: Model<StoryDocument>; 724 Follow: Model<FollowDocument>; 725 Block: Model<BlockDocument>; 726 Profile: Model<ProfileDocument>; 727 Audio: Model<AudioDocument>; 728 Repost: Model<RepostDocument>; 729 Generator: Model<GeneratorDocument>; 730 Labeler: Model<LabelerDocument>; 731 Label: Model<LabelDocument>; 732 Takedown: Model<TakedownDocument>; 733 RepoTakedown: Model<RepoTakedownDocument>; 734 BlobTakedown: Model<BlobTakedownDocument>; 735 Actor: Model<ActorDocument>; 736 ActorSync: Model<ActorSyncDocument>; 737 Preference: Model<PreferenceDocument>; 738 CursorState: Model<CursorStateDocument>; 739 Notification: Model<NotificationDocument>; 740 PushToken: Model<PushTokenDocument>; 741}