[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.

getfeedgen and prefs (#51)

* getfeedgen and prefs

* fmt

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* rm did field

* fmt

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

authored by

Roscoe Rubin-Rottenberg
Copilot
and committed by
GitHub
9b895e40 0162a7b2

+483 -47
+4
api/index.ts
··· 25 25 import getSuggestedFeeds from "./so/sprk/feed/getSuggestedFeeds.ts"; 26 26 import getTimeline from "./so/sprk/feed/getTimeline.ts"; 27 27 import getFeed from "./so/sprk/feed/getFeed.ts"; 28 + import getFeedGenerator from "./so/sprk/feed/getFeedGenerator.ts"; 29 + import getFeedGenerators from "./so/sprk/feed/getFeedGenerators.ts"; 28 30 29 31 export default function (server: Server, ctx: AppContext) { 30 32 getAccountInfos(server, ctx); ··· 52 54 getSuggestedFeeds(server, ctx); 53 55 getTimeline(server, ctx); 54 56 getFeed(server, ctx); 57 + getFeedGenerator(server, ctx); 58 + getFeedGenerators(server, ctx); 55 59 }
+112 -7
api/so/sprk/actor/getPreferences.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../context.ts"; 3 - import { Preferences } from "../../../../lex/types/so/sprk/actor/defs.ts"; 3 + import { 4 + ContentLabelPref, 5 + MutedWord, 6 + PostInteractionSettingsPref, 7 + Preferences, 8 + SavedFeed, 9 + ThreadViewPref, 10 + } from "../../../../lex/types/so/sprk/actor/defs.ts"; 4 11 5 12 export default function (server: Server, ctx: AppContext) { 6 13 server.so.sprk.actor.getPreferences({ ··· 9 16 const userDid = auth.credentials.iss; 10 17 11 18 try { 12 - const userPref = await ctx.db.models.UserPreference.findOne({ 19 + const userPref = await ctx.dataplane.preferences.getPreferences( 13 20 userDid, 14 - }); 21 + ); 22 + 23 + const preferences: Preferences = []; 24 + 25 + if (!userPref) { 26 + return { 27 + encoding: "application/json", 28 + body: { preferences }, 29 + }; 30 + } 31 + 32 + if (userPref.contentLabelPrefs?.length) { 33 + for (const pref of userPref.contentLabelPrefs) { 34 + preferences.push({ 35 + $type: "so.sprk.actor.defs#contentLabelPref", 36 + labelerDid: pref.labelerDid, 37 + label: pref.label, 38 + visibility: pref.visibility as ContentLabelPref["visibility"], 39 + }); 40 + } 41 + } 42 + 43 + if (userPref.savedFeeds?.length) { 44 + preferences.push({ 45 + $type: "so.sprk.actor.defs#savedFeedsPref", 46 + items: userPref.savedFeeds.map((item) => ({ 47 + ...item, 48 + type: item.type as SavedFeed["type"], 49 + })), 50 + }); 51 + } 52 + 53 + if (userPref.personalDetailsPref) { 54 + preferences.push({ 55 + $type: "so.sprk.actor.defs#personalDetailsPref", 56 + birthDate: userPref.personalDetailsPref.birthDate, 57 + }); 58 + } 59 + 60 + if (userPref.feedViewPrefs?.length) { 61 + for (const pref of userPref.feedViewPrefs) { 62 + preferences.push({ 63 + $type: "so.sprk.actor.defs#feedViewPref", 64 + feed: pref.feed, 65 + hideReplies: pref.hideReplies, 66 + hideRepliesByUnfollowed: pref.hideRepliesByUnfollowed, 67 + hideRepliesByLikeCount: pref.hideRepliesByLikeCount, 68 + hideRepliesByLookCount: pref.hideRepliesByLookCount, 69 + hideReposts: pref.hideReposts, 70 + hideQuotePosts: pref.hideQuotePosts, 71 + }); 72 + } 73 + } 74 + 75 + if (userPref.threadViewPref) { 76 + preferences.push({ 77 + $type: "so.sprk.actor.defs#threadViewPref", 78 + sort: userPref.threadViewPref.sort as ThreadViewPref["sort"], 79 + }); 80 + } 81 + 82 + if (userPref.interestsPref) { 83 + preferences.push({ 84 + $type: "so.sprk.actor.defs#interestsPref", 85 + tags: userPref.interestsPref.tags, 86 + }); 87 + } 88 + 89 + if (userPref.mutedWordsPref) { 90 + preferences.push({ 91 + $type: "so.sprk.actor.defs#mutedWordsPref", 92 + items: userPref.mutedWordsPref.items.map((item) => ({ 93 + ...item, 94 + targets: item.targets as MutedWord["targets"], 95 + actorTarget: item.actorTarget as MutedWord["actorTarget"], 96 + })), 97 + }); 98 + } 99 + 100 + if (userPref.hiddenPostsPref) { 101 + preferences.push({ 102 + $type: "so.sprk.actor.defs#hiddenPostsPref", 103 + items: userPref.hiddenPostsPref.items, 104 + }); 105 + } 106 + 107 + if (userPref.labelersPref) { 108 + preferences.push({ 109 + $type: "so.sprk.actor.defs#labelersPref", 110 + labelers: userPref.labelersPref.labelers, 111 + }); 112 + } 113 + 114 + if (userPref.postInteractionSettingsPref) { 115 + preferences.push({ 116 + $type: "so.sprk.actor.defs#postInteractionSettingsPref", 117 + threadgateAllowRules: userPref.postInteractionSettingsPref 118 + .threadgateAllowRules as PostInteractionSettingsPref[ 119 + "threadgateAllowRules" 120 + ], 121 + }); 122 + } 15 123 16 124 return { 17 125 encoding: "application/json", 18 126 body: { 19 - preferences: [{ 20 - $type: "so.sprk.actor.defs#savedFeedsPref", 21 - items: (userPref?.savedFeeds ?? []), 22 - }] as Preferences, 127 + preferences, 23 128 }, 24 129 }; 25 130 } catch (error) {
+112 -21
api/so/sprk/actor/putPreferences.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 - import { SavedFeedsPref } from "../../../../lex/types/so/sprk/actor/defs.ts"; 2 + import { 3 + ContentLabelPref, 4 + FeedViewPref, 5 + HiddenPostsPref, 6 + InterestsPref, 7 + LabelersPref, 8 + MutedWordsPref, 9 + PersonalDetailsPref, 10 + PostInteractionSettingsPref, 11 + SavedFeedsPref, 12 + ThreadViewPref, 13 + } from "../../../../lex/types/so/sprk/actor/defs.ts"; 14 + import { PreferenceDocument } from "../../../../data-plane/db/models.ts"; 3 15 import { AppContext } from "../../../../context.ts"; 4 16 5 17 export default function (server: Server, ctx: AppContext) { ··· 11 23 12 24 try { 13 25 const now = new Date().toISOString(); 14 - let userPref = await ctx.db.models.UserPreference.findOne({ userDid }); 15 26 16 - for (const pref of body.preferences) { 17 - if (pref as SavedFeedsPref) { 18 - const savedFeedsPref = pref as SavedFeedsPref; 27 + const updateData: Partial<PreferenceDocument> = { 28 + updatedAt: now, 29 + }; 19 30 20 - const savedFeeds = savedFeedsPref.items; 31 + // Track which preference types we've seen to collect all entries 32 + const contentLabelPrefs: NonNullable< 33 + PreferenceDocument["contentLabelPrefs"] 34 + > = []; 35 + const feedViewPrefs: NonNullable< 36 + PreferenceDocument["feedViewPrefs"] 37 + > = []; 21 38 22 - if (!userPref) { 23 - userPref = await ctx.db.models.UserPreference.create({ 24 - userDid, 25 - savedFeeds: savedFeeds, 26 - createdAt: now, 27 - updatedAt: now, 39 + for (const pref of body.preferences) { 40 + switch (pref.$type) { 41 + case "so.sprk.actor.defs#contentLabelPref": { 42 + const p = pref as ContentLabelPref; 43 + contentLabelPrefs.push({ 44 + labelerDid: p.labelerDid, 45 + label: p.label, 46 + visibility: p.visibility, 47 + }); 48 + break; 49 + } 50 + case "so.sprk.actor.defs#savedFeedsPref": { 51 + const p = pref as SavedFeedsPref; 52 + updateData.savedFeeds = p.items ?? []; 53 + break; 54 + } 55 + case "so.sprk.actor.defs#personalDetailsPref": { 56 + const p = pref as PersonalDetailsPref; 57 + updateData.personalDetailsPref = { 58 + birthDate: p.birthDate, 59 + }; 60 + break; 61 + } 62 + case "so.sprk.actor.defs#feedViewPref": { 63 + const p = pref as FeedViewPref; 64 + feedViewPrefs.push({ 65 + feed: p.feed, 66 + hideReplies: p.hideReplies, 67 + hideRepliesByUnfollowed: p.hideRepliesByUnfollowed, 68 + hideRepliesByLikeCount: p.hideRepliesByLikeCount, 69 + hideRepliesByLookCount: p.hideRepliesByLookCount, 70 + hideReposts: p.hideReposts, 71 + hideQuotePosts: p.hideQuotePosts, 28 72 }); 29 - } else { 30 - await ctx.db.models.UserPreference.updateOne( 31 - { userDid }, 32 - { 33 - $push: { 34 - savedFeeds: { $each: savedFeeds }, 35 - }, 36 - }, 37 - ); 73 + break; 74 + } 75 + case "so.sprk.actor.defs#threadViewPref": { 76 + const p = pref as ThreadViewPref; 77 + updateData.threadViewPref = { 78 + sort: p.sort, 79 + }; 80 + break; 81 + } 82 + case "so.sprk.actor.defs#interestsPref": { 83 + const p = pref as InterestsPref; 84 + updateData.interestsPref = { 85 + tags: p.tags, 86 + }; 87 + break; 88 + } 89 + case "so.sprk.actor.defs#mutedWordsPref": { 90 + const p = pref as MutedWordsPref; 91 + updateData.mutedWordsPref = { 92 + items: p.items ?? [], 93 + }; 94 + break; 95 + } 96 + case "so.sprk.actor.defs#hiddenPostsPref": { 97 + const p = pref as HiddenPostsPref; 98 + updateData.hiddenPostsPref = { 99 + items: p.items ?? [], 100 + }; 101 + break; 102 + } 103 + case "so.sprk.actor.defs#labelersPref": { 104 + const p = pref as LabelersPref; 105 + updateData.labelersPref = { 106 + labelers: p.labelers ?? [], 107 + }; 108 + break; 109 + } 110 + case "so.sprk.actor.defs#postInteractionSettingsPref": { 111 + const p = pref as PostInteractionSettingsPref; 112 + updateData.postInteractionSettingsPref = { 113 + threadgateAllowRules: p.threadgateAllowRules as Array<{ 114 + $type: string; 115 + [key: string]: unknown; 116 + }>, 117 + }; 118 + break; 38 119 } 39 120 } 40 121 } 122 + 123 + // Set array-based preferences if we found any 124 + if (contentLabelPrefs.length > 0) { 125 + updateData.contentLabelPrefs = contentLabelPrefs; 126 + } 127 + if (feedViewPrefs.length > 0) { 128 + updateData.feedViewPrefs = feedViewPrefs; 129 + } 130 + 131 + await ctx.dataplane.preferences.putPreferences(userDid, updateData); 41 132 42 133 return; 43 134 } catch (error) {
+48
api/so/sprk/feed/getFeedGenerator.ts
··· 1 + import { AppContext } from "../../../../context.ts"; 2 + import { Server } from "../../../../lex/index.ts"; 3 + import { resHeaders } from "../../../util.ts"; 4 + 5 + export default function (server: Server, ctx: AppContext) { 6 + server.so.sprk.feed.getFeedGenerator({ 7 + auth: ctx.authVerifier.optionalStandardOrRole, 8 + handler: async ({ params, auth }) => { 9 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 + const hydrateCtx = ctx.hydrator.createContext({ 11 + viewer, 12 + includeTakedowns, 13 + }); 14 + 15 + // Hydrate feed generator 16 + const hydrationState = await ctx.hydrator.hydrateFeedGens( 17 + [params.feed], 18 + hydrateCtx, 19 + ); 20 + 21 + // Create generator view 22 + const view = ctx.views.generator(params.feed, hydrationState); 23 + 24 + if (!view) { 25 + throw new Error(`Feed generator not found: ${params.feed}`); 26 + } 27 + 28 + // For now, assume online and valid 29 + // In a real implementation, you might check service health 30 + const isOnline = true; 31 + const isValid = true; 32 + 33 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 34 + 35 + return { 36 + encoding: "application/json", 37 + body: { 38 + view, 39 + isOnline, 40 + isValid, 41 + }, 42 + headers: resHeaders({ 43 + repoRev, 44 + }), 45 + }; 46 + }, 47 + }); 48 + }
+37
api/so/sprk/feed/getFeedGenerators.ts
··· 1 + import { AppContext } from "../../../../context.ts"; 2 + import { Server } from "../../../../lex/index.ts"; 3 + import { resHeaders } from "../../../util.ts"; 4 + 5 + export default function (server: Server, ctx: AppContext) { 6 + server.so.sprk.feed.getFeedGenerators({ 7 + auth: ctx.authVerifier.optionalStandardOrRole, 8 + handler: async ({ params, auth }) => { 9 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 + const hydrateCtx = ctx.hydrator.createContext({ 11 + viewer, 12 + includeTakedowns, 13 + }); 14 + 15 + // Hydrate feed generators 16 + const hydrationState = await ctx.hydrator.hydrateFeedGens( 17 + params.feeds, 18 + hydrateCtx, 19 + ); 20 + 21 + // Create generator views 22 + const feeds = params.feeds 23 + .map((uri) => ctx.views.generator(uri, hydrationState)) 24 + .filter((view): view is NonNullable<typeof view> => view !== undefined); 25 + 26 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 27 + 28 + return { 29 + encoding: "application/json", 30 + body: { feeds }, 31 + headers: resHeaders({ 32 + repoRev, 33 + }), 34 + }; 35 + }, 36 + }); 37 + }
+4 -4
data-plane/db/index.ts
··· 97 97 models.repostSchema, 98 98 ), 99 99 Generator: this.connection.model<models.GeneratorDocument>( 100 - "SprkGenerator", 100 + "Generator", 101 101 models.generatorSchema, 102 102 ), 103 103 Takedown: this.connection.model<models.TakedownDocument>( ··· 120 120 "ActorSync", 121 121 models.actorSyncSchema, 122 122 ), 123 - UserPreference: this.connection.model<models.UserPreferenceDocument>( 124 - "UserPreference", 125 - models.userPreferenceSchema, 123 + Preference: this.connection.model<models.PreferenceDocument>( 124 + "Preference", 125 + models.preferenceSchema, 126 126 ), 127 127 CursorState: this.connection.model<models.CursorStateDocument>( 128 128 "CursorState",
+65 -12
data-plane/db/models.ts
··· 474 474 services: { type: String, required: true }, 475 475 }); 476 476 477 - type SavedFeed = { 478 - id: string; 479 - type: "feed" | "list" | "timeline"; 480 - value: string; 481 - pinned: boolean; 482 - }; 483 - 484 - export interface UserPreferenceDocument extends Document { 477 + export interface PreferenceDocument extends Document { 485 478 userDid: string; 486 - savedFeeds: SavedFeed[]; 479 + contentLabelPrefs?: Array<{ 480 + labelerDid?: string; 481 + label: string; 482 + visibility: string; 483 + }>; 484 + savedFeeds?: Array<{ 485 + id: string; 486 + type: string; 487 + value: string; 488 + pinned: boolean; 489 + }>; 490 + personalDetailsPref?: { 491 + birthDate?: string; 492 + }; 493 + feedViewPrefs?: Array<{ 494 + feed: string; 495 + hideReplies?: boolean; 496 + hideRepliesByUnfollowed: boolean; 497 + hideRepliesByLikeCount?: number; 498 + hideRepliesByLookCount?: number; 499 + hideReposts?: boolean; 500 + hideQuotePosts?: boolean; 501 + }>; 502 + threadViewPref?: { 503 + sort?: string; 504 + }; 505 + interestsPref?: { 506 + tags: string[]; 507 + }; 508 + mutedWordsPref?: { 509 + items: Array<{ 510 + id?: string; 511 + value: string; 512 + targets: string[]; 513 + actorTarget: string; 514 + expiresAt?: string; 515 + }>; 516 + }; 517 + hiddenPostsPref?: { 518 + items: string[]; 519 + }; 520 + labelersPref?: { 521 + labelers: Array<{ 522 + did: string; 523 + }>; 524 + }; 525 + postInteractionSettingsPref?: { 526 + threadgateAllowRules?: Array<{ 527 + $type: string; 528 + [key: string]: unknown; 529 + }>; 530 + }; 487 531 createdAt: string; 488 532 updatedAt: string; 489 533 } 490 534 491 - export const userPreferenceSchema = new Schema<UserPreferenceDocument>({ 535 + export const preferenceSchema = new Schema<PreferenceDocument>({ 492 536 userDid: { type: String, required: true, unique: true, index: true }, 493 - savedFeeds: { type: [Object], required: true }, 537 + contentLabelPrefs: { type: [Object], required: false }, 538 + savedFeeds: { type: [Object], required: false }, 539 + personalDetailsPref: { type: Object, required: false }, 540 + feedViewPrefs: { type: [Object], required: false }, 541 + threadViewPref: { type: Object, required: false }, 542 + interestsPref: { type: Object, required: false }, 543 + mutedWordsPref: { type: Object, required: false }, 544 + hiddenPostsPref: { type: Object, required: false }, 545 + labelersPref: { type: Object, required: false }, 546 + postInteractionSettingsPref: { type: Object, required: false }, 494 547 createdAt: { type: String, required: true }, 495 548 updatedAt: { type: String, required: true }, 496 549 }); ··· 539 592 BlobTakedown: Model<BlobTakedownDocument>; 540 593 Actor: Model<ActorDocument>; 541 594 ActorSync: Model<ActorSyncDocument>; 542 - UserPreference: Model<UserPreferenceDocument>; 595 + Preference: Model<PreferenceDocument>; 543 596 CursorState: Model<CursorStateDocument>; 544 597 }
+3
data-plane/index.ts
··· 15 15 import { Stories } from "./routes/stories.ts"; 16 16 import { Sync } from "./routes/sync.ts"; 17 17 import { Threads } from "./routes/threads.ts"; 18 + import { Preferences } from "./routes/preferences.ts"; 18 19 19 20 export { RepoSubscription } from "./subscription.ts"; 20 21 ··· 43 44 public stories: Stories; 44 45 public sync: Sync; 45 46 public threads: Threads; 47 + public preferences: Preferences; 46 48 47 49 constructor( 48 50 db: Database, ··· 67 69 this.stories = new Stories(db); 68 70 this.sync = new Sync(db); 69 71 this.threads = new Threads(db); 72 + this.preferences = new Preferences(db); 70 73 } 71 74 }
+25
data-plane/routes/feeds.ts
··· 37 37 this.timeCidKeyset = new TimeCidKeyset(); 38 38 } 39 39 40 + async getFeedGenerators(uris: string[]) { 41 + if (!uris.length) return { generators: [] }; 42 + 43 + const generators = await this.db.models.Generator.find({ 44 + uri: { $in: uris }, 45 + }).populate("actor"); 46 + 47 + return { 48 + generators: generators.map((generator) => ({ 49 + uri: generator.uri, 50 + cid: generator.cid, 51 + authorDid: generator.authorDid, 52 + displayName: generator.displayName, 53 + description: generator.description, 54 + descriptionFacets: generator.descriptionFacets, 55 + avatar: generator.avatar, 56 + acceptsInteractions: generator.acceptsInteractions, 57 + likeCount: generator.likeCount || 0, 58 + createdAt: generator.createdAt, 59 + indexedAt: generator.indexedAt, 60 + actor: generator.actor, 61 + })), 62 + }; 63 + } 64 + 40 65 async getAuthorFeed( 41 66 actorDid: string, 42 67 limit = 50,
+32
data-plane/routes/preferences.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { PreferenceDocument } from "../db/models.ts"; 3 + 4 + export class Preferences { 5 + private db: Database; 6 + 7 + constructor(db: Database) { 8 + this.db = db; 9 + } 10 + 11 + async getPreferences(userDid: string) { 12 + return await this.db.models.Preference.findOne({ userDid }); 13 + } 14 + 15 + async putPreferences(userDid: string, data: Partial<PreferenceDocument>) { 16 + const now = new Date().toISOString(); 17 + 18 + const updateData = { 19 + ...data, 20 + updatedAt: now, 21 + }; 22 + 23 + await this.db.models.Preference.findOneAndUpdate( 24 + { userDid }, 25 + { 26 + $set: updateData, 27 + $setOnInsert: { userDid, createdAt: now }, 28 + }, 29 + { upsert: true }, 30 + ); 31 + } 32 + }
+2 -3
hydration/feed.ts
··· 1 1 import { Record as FeedGenRecord } from "../lex/types/so/sprk/feed/generator.ts"; 2 - import { Record as BskyFeedGenRecord } from "../lex/types/app/bsky/feed/generator.ts"; 3 2 import { Record as LikeRecord } from "../lex/types/so/sprk/feed/like.ts"; 4 3 import { Record as PostRecord } from "../lex/types/so/sprk/feed/post.ts"; 5 4 import { Record as ReplyRecord } from "../lex/types/so/sprk/feed/reply.ts"; ··· 70 69 71 70 export type FeedGenAggs = HydrationMap<FeedGenAgg>; 72 71 73 - export type FeedGen = RecordInfo<FeedGenRecord | BskyFeedGenRecord>; 72 + export type FeedGen = RecordInfo<FeedGenRecord>; 74 73 export type FeedGens = HydrationMap<FeedGen>; 75 74 76 75 export type FeedGenViewerState = { ··· 268 267 if (!uris.length) return new HydrationMap<FeedGen>(); 269 268 const res = await this.dataplane.records.getFeedGeneratorRecords(uris); 270 269 return uris.reduce((acc, uri, i) => { 271 - const record = parseRecord<FeedGenRecord | BskyFeedGenRecord>( 270 + const record = parseRecord<FeedGenRecord>( 272 271 res.records[i], 273 272 includeTakedowns, 274 273 );
+39
views/index.ts
··· 27 27 } from "../lex/types/so/sprk/actor/defs.ts"; 28 28 import { 29 29 BlockedPost, 30 + GeneratorView, 30 31 ImagesMedia, 31 32 ImagesMediaView, 32 33 isImagesMedia, ··· 752 753 } 753 754 : undefined, 754 755 indexedAt: this.indexedAt(soundInfo)?.toISOString() ?? 756 + new Date().toISOString(), 757 + }; 758 + } 759 + 760 + generator( 761 + uri: string, 762 + state: HydrationState, 763 + ): Un$Typed<GeneratorView> | undefined { 764 + const generatorInfo = state.feedgens?.get(uri); 765 + if (!generatorInfo) return; 766 + 767 + const parsedUri = new AtUri(uri); 768 + const authorDid = parsedUri.hostname; 769 + const creator = this.profile(authorDid, state); 770 + if (!creator) return; 771 + 772 + const generatorAgg = state.feedgenAggs?.get(uri); 773 + const viewer = state.feedgenViewers?.get(uri); 774 + 775 + const avatar = generatorInfo.record.avatar 776 + ? `${this.mediaCdn}/avatar/medium/${authorDid}/${ 777 + cidFromBlobJson(generatorInfo.record.avatar as BlobRef) 778 + }/webp` 779 + : undefined; 780 + 781 + return { 782 + uri, 783 + cid: generatorInfo.cid, 784 + did: generatorInfo.record.did, 785 + creator, 786 + displayName: generatorInfo.record.displayName, 787 + description: generatorInfo.record.description, 788 + descriptionFacets: generatorInfo.record.descriptionFacets, 789 + avatar, 790 + likeCount: generatorAgg?.likes ?? 0, 791 + acceptsInteractions: generatorInfo.record.acceptsInteractions, 792 + viewer: viewer?.like ? { like: viewer.like } : undefined, 793 + indexedAt: this.indexedAt(generatorInfo)?.toISOString() ?? 755 794 new Date().toISOString(), 756 795 }; 757 796 }