[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 17a4453b336a3693a4e2b55eced2d8a4a0cf1bd5 629 lines 17 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// 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}