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

Configure Feed

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

chore: upgrade to mongoose 9

+95 -119
+23 -16
data-plane/db/models.ts
··· 1 - import { Document, Model, Schema } from "mongoose"; 1 + import { Model, Schema } from "mongoose"; 2 2 3 3 interface RecordRef { 4 4 uri: string; ··· 23 23 } 24 24 25 25 // Base interface for documents with authorDid 26 - interface AuthoredDocument extends Document { 26 + interface AuthoredDocument { 27 27 uri: string; 28 28 cid: string; 29 29 createdAt: string; ··· 43 43 export interface MediaRef { 44 44 $type: string; 45 45 ref: { $link: string }; 46 + mimeType?: string; 47 + size?: number; 46 48 } 47 49 export interface ImageMedia extends MediaRef { 48 50 alt: string; ··· 86 88 $type: string; 87 89 video?: VideoMedia; 88 90 image?: ImageMedia; 91 + alt?: string; 92 + aspectRatio?: { 93 + width: number; 94 + height: number; 95 + }; 89 96 } 90 97 export interface Caption { 91 98 text: string; ··· 94 101 95 102 // records 96 103 97 - export interface RecordDocument extends Document { 104 + export interface RecordDocument { 98 105 uri: string; 99 106 cid: string; 100 107 did: string; ··· 108 115 invalidReplyRoot?: boolean; 109 116 } 110 117 111 - export interface ArchivedRecordDocument extends Document { 118 + export interface ArchivedRecordDocument { 112 119 uri: string; 113 120 cid: string; 114 121 did: string; ··· 156 163 157 164 // duplicate records 158 165 159 - export interface DuplicateRecordDocument extends Document { 166 + export interface DuplicateRecordDocument { 160 167 uri: string; 161 168 cid: string; 162 169 duplicateOf: string; ··· 171 178 172 179 // actor sync 173 180 174 - export interface ActorSyncDocument extends Document { 181 + export interface ActorSyncDocument { 175 182 did: string; 176 183 commitCid: string; 177 184 repoRev: string | null; ··· 348 355 root: RecordRef; 349 356 parent: RecordRef; 350 357 }; 351 - media?: ImageMedia; 358 + media?: ImageMedia | { images?: ImageMedia[]; [key: string]: unknown }; 352 359 langs?: string[]; 353 360 labels?: Label[]; 354 361 likeCount: number; ··· 436 443 437 444 // labels 438 445 439 - export interface LabelDocument extends Document { 446 + export interface LabelDocument { 440 447 src: string; 441 448 uri: string; 442 449 cid: string; ··· 460 467 461 468 // takedowns 462 469 463 - export interface TakedownDocument extends Document { 470 + export interface TakedownDocument { 464 471 targetUri: string; 465 472 targetCid: string; 466 473 reason: string; ··· 481 488 482 489 // repo takedowns 483 490 484 - export interface RepoTakedownDocument extends Document { 491 + export interface RepoTakedownDocument { 485 492 did: string; 486 493 reason: string; 487 494 takenDownBy: string; ··· 500 507 501 508 // blobs takedowns 502 509 503 - export interface BlobTakedownDocument extends Document { 510 + export interface BlobTakedownDocument { 504 511 did: string; 505 512 cid: string; 506 513 reason: string; ··· 522 529 523 530 // actors 524 531 525 - export interface ActorDocument extends Document { 532 + export interface ActorDocument { 526 533 did: string; 527 534 handle: string | null; 528 535 indexedAt: string; ··· 545 552 546 553 // preferences 547 554 548 - export interface PreferenceDocument extends Document { 555 + export interface PreferenceDocument { 549 556 userDid: string; 550 557 contentLabelPrefs?: Array<{ 551 558 labelerDid?: string; ··· 619 626 620 627 // cursor state 621 628 622 - export interface CursorStateDocument extends Document { 629 + export interface CursorStateDocument { 623 630 identifier: string; // To ensure a single document, e.g., 'last_processed_cursor' 624 631 cursorValue: number; 625 632 updatedAt: Date; ··· 632 639 633 640 // notifications 634 641 635 - export interface NotificationDocument extends Document { 642 + export interface NotificationDocument { 636 643 did: string; 637 644 recordUri: string; 638 645 recordCid: string; ··· 655 662 656 663 // push tokens 657 664 658 - export interface PushTokenDocument extends Document { 665 + export interface PushTokenDocument { 659 666 did: string; 660 667 token: string; 661 668 platform: "ios" | "android" | "web";
+7 -7
data-plane/db/pagination.ts
··· 1 1 import { ensureValidRecordKey } from "@atp/syntax"; 2 2 import { InvalidRequestError } from "@atp/xrpc-server"; 3 - import { Document, FilterQuery, Query } from "mongoose"; 3 + import { Document, Query, QueryFilter } from "mongoose"; 4 4 5 5 type KeysetCursor = { primary: string; secondary: string }; 6 6 type KeysetLabeledResult = { ··· 71 71 getFilter<T>( 72 72 labeled?: LR, 73 73 direction?: "asc" | "desc", 74 - ): FilterQuery<T> | undefined { 74 + ): QueryFilter<T> | undefined { 75 75 if (labeled === undefined) return undefined; 76 76 77 77 // MongoDB compound key comparison using $or ··· 84 84 [this.secondary]: { $gt: labeled.secondary }, 85 85 }, 86 86 ], 87 - } as FilterQuery<T>; 87 + } as QueryFilter<T>; 88 88 } else { 89 89 return { 90 90 $or: [ ··· 94 94 [this.secondary]: { $lt: labeled.secondary }, 95 95 }, 96 96 ], 97 - } as FilterQuery<T>; 97 + } as QueryFilter<T>; 98 98 } 99 99 } 100 100 paginate<T extends Document>( ··· 274 274 getFilter<T>( 275 275 labeled?: LR, 276 276 direction?: "asc" | "desc", 277 - ): FilterQuery<T> | undefined { 277 + ): QueryFilter<T> | undefined { 278 278 if (labeled === undefined) return undefined; 279 279 if (direction === "asc") { 280 - return { [this.primary]: { $gt: labeled.primary } } as FilterQuery<T>; 280 + return { [this.primary]: { $gt: labeled.primary } } as QueryFilter<T>; 281 281 } 282 - return { [this.primary]: { $lt: labeled.primary } } as FilterQuery<T>; 282 + return { [this.primary]: { $lt: labeled.primary } } as QueryFilter<T>; 283 283 } 284 284 paginate<T extends Document>( 285 285 query: Query<T[], T>,
+13 -13
data-plane/db/util.ts
··· 1 - import { FilterQuery } from "mongoose"; 1 + import { QueryFilter } from "mongoose"; 2 2 3 3 // MongoDB query builder for actor matching (DID or handle) 4 - export const actorFilter = <T>(actor: string): FilterQuery<T> => { 4 + export const actorFilter = <T>(actor: string): QueryFilter<T> => { 5 5 if (actor.startsWith("did:")) { 6 - return { did: actor } as FilterQuery<T>; 6 + return { did: actor } as QueryFilter<T>; 7 7 } else { 8 - return { handle: actor } as FilterQuery<T>; 8 + return { handle: actor } as QueryFilter<T>; 9 9 } 10 10 }; 11 11 12 12 // Filter for documents that are not soft deleted 13 - export const notSoftDeletedFilter = <T>(): FilterQuery<T> => { 14 - return { takedownRef: { $exists: false } } as FilterQuery<T>; 13 + export const notSoftDeletedFilter = <T>(): QueryFilter<T> => { 14 + return { takedownRef: { $exists: false } } as QueryFilter<T>; 15 15 }; 16 16 17 17 // Check if a document is soft deleted ··· 26 26 field: string, 27 27 start?: Date, 28 28 end?: Date, 29 - ): FilterQuery<T> => { 29 + ): QueryFilter<T> => { 30 30 const filter: Record<string, unknown> = {}; 31 31 if (start || end) { 32 32 filter[field] = {}; 33 33 if (start) (filter[field] as Record<string, unknown>).$gte = start; 34 34 if (end) (filter[field] as Record<string, unknown>).$lte = end; 35 35 } 36 - return filter as FilterQuery<T>; 36 + return filter as QueryFilter<T>; 37 37 }; 38 38 39 39 // Helper for pagination ··· 45 45 46 46 // Helper for creating compound filters 47 47 export const andFilter = <T>( 48 - ...filters: FilterQuery<T>[] 49 - ): FilterQuery<T> => ({ 48 + ...filters: QueryFilter<T>[] 49 + ): QueryFilter<T> => ({ 50 50 $and: filters.filter((f) => Object.keys(f).length > 0), 51 - } as FilterQuery<T>); 51 + } as QueryFilter<T>); 52 52 53 - export const orFilter = <T>(...filters: FilterQuery<T>[]): FilterQuery<T> => ({ 53 + export const orFilter = <T>(...filters: QueryFilter<T>[]): QueryFilter<T> => ({ 54 54 $or: filters.filter((f) => Object.keys(f).length > 0), 55 - } as FilterQuery<T>); 55 + } as QueryFilter<T>);
+2 -2
data-plane/indexing/plugins/audio.ts
··· 32 32 33 33 // Use findOneAndUpdate with upsert to handle potential duplicate key errors 34 34 const insertedAudio = await db.models.Audio.findOneAndUpdate( 35 - { uri: audio.uri }, 35 + { uri: uri.toString() }, 36 36 { $set: audio }, 37 - { upsert: true, new: true }, 37 + { upsert: true, new: true, includeResultMetadata: false }, 38 38 ); 39 39 return insertedAudio; 40 40 };
+2 -2
deno.json
··· 22 22 "@atp/xrpc": "jsr:@atp/xrpc@^0.1.0-alpha.4", 23 23 "@atp/xrpc-server": "jsr:@atp/xrpc-server@^0.1.0-alpha.9", 24 24 "@std/assert": "jsr:@std/assert@^1.0.18", 25 - "dotenv": "npm:dotenv@^17.2.4", 25 + "dotenv": "npm:dotenv@^17.3.1", 26 26 "hono": "jsr:@hono/hono@^4.11.9", 27 27 "@std/encoding": "jsr:@std/encoding@^1.0.10", 28 28 "@atproto/api": "npm:@atproto/api@^0.18.21", 29 29 "jose": "npm:jose@^6.1.3", 30 - "mongoose": "npm:mongoose@^8.23.0", 30 + "mongoose": "npm:mongoose@^9.2.1", 31 31 "multiformats": "npm:multiformats@^13.4.2", 32 32 "p-queue": "npm:p-queue@^9.1.0", 33 33 "mongodb-memory-server-core": "npm:mongodb-memory-server-core@^11.0.1",
+17 -45
deno.lock
··· 34 34 "npm:@bufbuild/protobuf@1.5.0": "1.5.0", 35 35 "npm:@ipld/dag-cbor@^9.2.5": "9.2.5", 36 36 "npm:@opentelemetry/api@^1.9.0": "1.9.0", 37 - "npm:dotenv@^17.2.4": "17.2.4", 37 + "npm:dotenv@^17.3.1": "17.3.1", 38 38 "npm:jose@^6.1.3": "6.1.3", 39 39 "npm:lodash@*": "4.17.21", 40 40 "npm:mongodb-memory-server-core@^11.0.1": "11.0.1", 41 - "npm:mongoose@^8.23.0": "8.23.0", 41 + "npm:mongoose@^9.2.1": "9.2.1", 42 42 "npm:multiformats@^13.4.1": "13.4.2", 43 43 "npm:multiformats@^13.4.2": "13.4.2", 44 44 "npm:p-queue@^8.1.1": "8.1.1", ··· 403 403 "@types/webidl-conversions@7.0.3": { 404 404 "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" 405 405 }, 406 - "@types/whatwg-url@11.0.5": { 407 - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 408 - "dependencies": [ 409 - "@types/webidl-conversions" 410 - ] 411 - }, 412 406 "@types/whatwg-url@13.0.0": { 413 407 "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", 414 408 "dependencies": [ ··· 471 465 "type-is", 472 466 "unpipe" 473 467 ] 474 - }, 475 - "bson@6.10.4": { 476 - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" 477 468 }, 478 469 "bson@7.1.1": { 479 470 "integrity": "sha512-TtJgBB+QyOlWjrbM+8bRgH84VM/xrDjyBFgSgGrfZF4xvt6gbEDtcswm27Tn9F9TWsjQybxT8b8VpCP/oJK4Dw==" ··· 552 543 "destroy@1.2.0": { 553 544 "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 554 545 }, 555 - "dotenv@17.2.4": { 556 - "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==" 546 + "dotenv@17.3.1": { 547 + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==" 557 548 }, 558 549 "dunder-proto@1.0.1": { 559 550 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", ··· 758 749 "jose@6.1.3": { 759 750 "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==" 760 751 }, 761 - "kareem@2.6.3": { 762 - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" 752 + "kareem@3.2.0": { 753 + "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==" 763 754 }, 764 755 "locate-path@5.0.0": { 765 756 "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", ··· 804 795 "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 805 796 "bin": true 806 797 }, 807 - "mongodb-connection-string-url@3.0.2": { 808 - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 809 - "dependencies": [ 810 - "@types/whatwg-url@11.0.5", 811 - "whatwg-url" 812 - ] 813 - }, 814 798 "mongodb-connection-string-url@7.0.1": { 815 799 "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", 816 800 "dependencies": [ 817 - "@types/whatwg-url@13.0.0", 801 + "@types/whatwg-url", 818 802 "whatwg-url" 819 803 ] 820 804 }, ··· 827 811 "find-cache-dir", 828 812 "follow-redirects", 829 813 "https-proxy-agent", 830 - "mongodb@7.0.0", 814 + "mongodb", 831 815 "new-find-package-json", 832 816 "semver@7.7.3", 833 817 "tar-stream", ··· 835 819 "yauzl" 836 820 ] 837 821 }, 838 - "mongodb@6.20.0": { 839 - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", 840 - "dependencies": [ 841 - "@mongodb-js/saslprep", 842 - "bson@6.10.4", 843 - "mongodb-connection-string-url@3.0.2" 844 - ] 845 - }, 846 822 "mongodb@7.0.0": { 847 823 "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", 848 824 "dependencies": [ 849 825 "@mongodb-js/saslprep", 850 - "bson@7.1.1", 851 - "mongodb-connection-string-url@7.0.1" 826 + "bson", 827 + "mongodb-connection-string-url" 852 828 ] 853 829 }, 854 - "mongoose@8.23.0": { 855 - "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", 830 + "mongoose@9.2.1": { 831 + "integrity": "sha512-fmNLwgct5km7iL1MqvTMncarR1E1TIw2lmc9A4UoDVdS7AQe95K+DnRK0qATkSUdwUC9V/5wlDcqnkQQjbSRkA==", 856 832 "dependencies": [ 857 - "bson@6.10.4", 858 833 "kareem", 859 - "mongodb@6.20.0", 834 + "mongodb", 860 835 "mpath", 861 836 "mquery", 862 837 "ms@2.1.3", ··· 866 841 "mpath@0.9.0": { 867 842 "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" 868 843 }, 869 - "mquery@5.0.0": { 870 - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", 871 - "dependencies": [ 872 - "debug@4.4.3" 873 - ] 844 + "mquery@6.0.0": { 845 + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==" 874 846 }, 875 847 "ms@2.0.0": { 876 848 "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" ··· 1282 1254 "jsr:@std/assert@^1.0.18", 1283 1255 "jsr:@std/encoding@^1.0.10", 1284 1256 "npm:@atproto/api@~0.18.21", 1285 - "npm:dotenv@^17.2.4", 1257 + "npm:dotenv@^17.3.1", 1286 1258 "npm:jose@^6.1.3", 1287 1259 "npm:mongodb-memory-server-core@^11.0.1", 1288 - "npm:mongoose@^8.23.0", 1260 + "npm:mongoose@^9.2.1", 1289 1261 "npm:multiformats@^13.4.2", 1290 1262 "npm:p-queue@^9.1.0", 1291 1263 "npm:structured-headers@^2.0.2"
+31 -34
tests/util.ts
··· 447 447 indexedAt: now, 448 448 takedownRef: "", 449 449 upstreamStatus: "active", 450 - keys: JSON.stringify({ signing: "key123" }), 450 + keys: ["key123"], 451 451 services: JSON.stringify({ pds: "https://pds.test" }), 452 452 }); 453 453 } ··· 487 487 size: 100000, 488 488 }, 489 489 labels: [], 490 - pinnedPost: "", 491 490 postsCount: 5, 492 491 followersCount: 100, 493 492 followsCount: 50, ··· 502 501 displayName: "Bob", 503 502 description: "Music lover and tech geek", 504 503 labels: [], 505 - pinnedPost: "", 506 504 postsCount: 3, 507 505 followersCount: 75, 508 506 followsCount: 80, ··· 517 515 displayName: "Charlie", 518 516 description: "Adventure seeker", 519 517 labels: [], 520 - pinnedPost: "", 521 518 postsCount: 10, 522 519 followersCount: 200, 523 520 followsCount: 150, ··· 539 536 mimeType: "audio/mpeg", 540 537 size: 1000000, 541 538 }, 542 - origin: "original", 539 + origin: { 540 + uri: `at://${TEST_USERS[1].did}/app.sprk.audio/origin1`, 541 + cid: `${baseCid}origin1`, 542 + }, 543 543 title: "Chill Beats", 544 - details: "Relaxing music for coding", 544 + details: { title: "Relaxing music for coding" }, 545 545 labels: [], 546 546 useCount: 10, 547 547 }); ··· 559 559 mimeType: "audio/mpeg", 560 560 size: 800000, 561 561 }, 562 - origin: "original", 562 + origin: { 563 + uri: `at://${TEST_USERS[1].did}/app.sprk.audio/origin2`, 564 + cid: `${baseCid}origin2`, 565 + }, 563 566 title: "Summer Vibes", 564 - details: "Upbeat summer track", 567 + details: { title: "Upbeat summer track" }, 565 568 labels: [], 566 569 useCount: 5, 567 570 }); ··· 922 925 if (options.preferences) { 923 926 await models.Preference.create({ 924 927 userDid: TEST_USERS[0].did, 925 - contentLabelPrefs: JSON.stringify({ 926 - labelerDids: [], 927 - labels: {}, 928 - }), 929 - savedFeeds: JSON.stringify([]), 930 - personalDetailsPref: JSON.stringify({}), 931 - feedViewPrefs: JSON.stringify({}), 932 - threadViewPref: JSON.stringify({}), 933 - interestsPref: JSON.stringify({ tags: ["tech", "music"] }), 934 - mutedWordsPref: JSON.stringify([]), 935 - hiddenPostsPref: JSON.stringify([]), 936 - labelersPref: JSON.stringify([]), 937 - postInteractionSettingsPref: JSON.stringify({}), 928 + contentLabelPrefs: [], 929 + savedFeeds: [], 930 + personalDetailsPref: {}, 931 + feedViewPrefs: [], 932 + threadViewPref: {}, 933 + interestsPref: { tags: ["tech", "music"] }, 934 + mutedWordsPref: { items: [] }, 935 + hiddenPostsPref: { items: [] }, 936 + labelersPref: { labelers: [] }, 937 + postInteractionSettingsPref: { threadgateAllowRules: [] }, 938 938 createdAt: now, 939 939 updatedAt: now, 940 940 }); 941 941 942 942 await models.Preference.create({ 943 943 userDid: TEST_USERS[1].did, 944 - contentLabelPrefs: JSON.stringify({ 945 - labelerDids: [], 946 - labels: {}, 947 - }), 948 - savedFeeds: JSON.stringify([]), 949 - personalDetailsPref: JSON.stringify({}), 950 - feedViewPrefs: JSON.stringify({}), 951 - threadViewPref: JSON.stringify({}), 952 - interestsPref: JSON.stringify({ tags: ["music", "art"] }), 953 - mutedWordsPref: JSON.stringify([]), 954 - hiddenPostsPref: JSON.stringify([]), 955 - labelersPref: JSON.stringify([]), 956 - postInteractionSettingsPref: JSON.stringify({}), 944 + contentLabelPrefs: [], 945 + savedFeeds: [], 946 + personalDetailsPref: {}, 947 + feedViewPrefs: [], 948 + threadViewPref: {}, 949 + interestsPref: { tags: ["music", "art"] }, 950 + mutedWordsPref: { items: [] }, 951 + hiddenPostsPref: { items: [] }, 952 + labelersPref: { labelers: [] }, 953 + postInteractionSettingsPref: { threadgateAllowRules: [] }, 957 954 createdAt: now, 958 955 updatedAt: now, 959 956 });