diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 45a669ee281..7dd1567d1ec 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -32,10 +32,10 @@ export default function (server: Server, ctx: AppContext) { } const relevant = getRelevantIds(threadData) const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester), + feedService.getActorViews(Array.from(relevant.dids), requester, true), feedService.getPostViews(Array.from(relevant.uris), requester), feedService.embedsForPosts(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects(Array.from(relevant.uris)), + labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) const thread = composeThread( diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index b2da51beb51..b93b9a34326 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { feedService.getActorViews(Array.from(dids), requester), feedService.getPostViews(Array.from(uris), requester), feedService.embedsForPosts(Array.from(uris), requester), - labelService.getLabelsForSubjects(Array.from(uris)), + labelService.getLabelsForUris(Array.from(uris)), ]) const posts: PostView[] = [] diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index bcad398942c..9b93285891c 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -68,7 +68,7 @@ export default function (server: Server, ctx: AppContext) { })), requester, ), - labelService.getLabelsForSubjects(recordUris), + labelService.getLabelsForUris(recordUris), ]) const notifications = notifs.map((notif, i) => ({ diff --git a/packages/bsky/src/api/app/bsky/util/feed.ts b/packages/bsky/src/api/app/bsky/util/feed.ts index cb6a271b664..3568475db4c 100644 --- a/packages/bsky/src/api/app/bsky/util/feed.ts +++ b/packages/bsky/src/api/app/bsky/util/feed.ts @@ -29,10 +29,10 @@ export const composeFeed = async ( } } const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(actorDids), viewer), + feedService.getActorViews(Array.from(actorDids), viewer, true), feedService.getPostViews(Array.from(postUris), viewer), feedService.embedsForPosts(Array.from(postUris), viewer), - labelService.getLabelsForSubjects(Array.from(postUris)), + labelService.getLabelsForSubjects([...postUris, ...actorDids]), ]) const feed: FeedViewPost[] = [] diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index ec8a0509706..8a37f6fe489 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -80,7 +80,7 @@ export class ActorViews { const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForProfiles(dids), + this.services.label(this.db).getLabelsForSubjects(dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { @@ -169,7 +169,7 @@ export class ActorViews { const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForProfiles(dids), + this.services.label(this.db).getLabelsForSubjects(dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 74b119dd84e..943654d03a2 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -70,6 +70,7 @@ export class FeedService { async getActorViews( dids: string[], viewer: string | null, + skipLabels?: boolean, // @NOTE used by composeFeed() to batch label hydration ): Promise { if (dids.length < 1) return {} const { ref } = this.db.db.dynamic @@ -101,9 +102,10 @@ export class FeedService { .as('requesterFollowedBy'), ]) .execute(), - this.services.label(this.db).getLabelsForProfiles(dids), + this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids), ]) return actors.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] return { ...acc, [cur.did]: { @@ -124,7 +126,7 @@ export class FeedService { // muted field hydrated on pds } : undefined, - labels: labels[cur.did] ?? [], + labels: skipLabels ? undefined : actorLabels, }, } }, {} as ActorViewMap) @@ -234,7 +236,7 @@ export class FeedService { viewer, ), this.embedsForPosts(nestedUris, viewer, _depth + 1), - this.services.label(this.db).getLabelsForSubjects(nestedUris), + this.services.label(this.db).getLabelsForUris(nestedUris), ]) let embeds = images.reduce((acc, cur) => { const embed = (acc[cur.postUri] ??= { @@ -341,6 +343,9 @@ export class FeedService { const post = posts[uri] const author = actors[post?.creator] if (!post || !author) return undefined + // If the author labels are not hydrated yet, attempt to pull them + // from labels: e.g. compatible with composeFeed() batching label hydration. + author.labels ??= labels[author.did] ?? [] return { $type: 'app.bsky.feed.defs#postView', uri: post.uri, diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index da17546bc30..b979799f126 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -60,7 +60,7 @@ export class LabelService { .execute() } - async getLabelsForSubjects( + async getLabelsForUris( subjects: string[], includeNeg?: boolean, ): Promise { @@ -82,28 +82,42 @@ export class LabelService { }, {} as Labels) } - // gets labels for both did & profile record - async getLabelsForProfiles( - dids: string[], + // gets labels for any record. when did is present, combine labels for both did & profile record. + async getLabelsForSubjects( + subjects: string[], includeNeg?: boolean, ): Promise { - if (dids.length < 1) return {} - const profileUris = dids.map((did) => - AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), - ) - const subjects = [...dids, ...profileUris] - const labels = await this.getLabelsForSubjects(subjects, includeNeg) - // combine labels for profile + did + if (subjects.length < 1) return {} + const expandedSubjects = subjects.flatMap((subject) => { + if (subject.startsWith('did:')) { + return [ + subject, + AtUri.make(subject, ids.AppBskyActorProfile, 'self').toString(), + ] + } + return subject + }) + const labels = await this.getLabelsForUris(expandedSubjects, includeNeg) return Object.keys(labels).reduce((acc, cur) => { - const did = cur.startsWith('at://') ? new AtUri(cur).hostname : cur - acc[did] ??= [] - acc[did] = [...acc[did], ...labels[cur]] + const uri = cur.startsWith('at://') ? new AtUri(cur) : null + if ( + uri && + uri.collection === ids.AppBskyActorProfile && + uri.rkey === 'self' + ) { + // combine labels for profile + did + const did = uri.hostname + acc[did] ??= [] + acc[did].push(...labels[cur]) + } + acc[cur] ??= [] + acc[cur].push(...labels[cur]) return acc }, {} as Labels) } async getLabels(subject: string, includeNeg?: boolean): Promise { - const labels = await this.getLabelsForSubjects([subject], includeNeg) + const labels = await this.getLabelsForUris([subject], includeNeg) return labels[subject] ?? [] } @@ -111,7 +125,7 @@ export class LabelService { did: string, includeNeg?: boolean, ): Promise { - const labels = await this.getLabelsForProfiles([did], includeNeg) + const labels = await this.getLabelsForSubjects([did], includeNeg) return labels[did] ?? [] } } diff --git a/packages/bsky/src/services/types.ts b/packages/bsky/src/services/types.ts index 9dc748d3e27..62a12448c4c 100644 --- a/packages/bsky/src/services/types.ts +++ b/packages/bsky/src/services/types.ts @@ -2,6 +2,7 @@ import { View as ViewImages } from '../lexicon/types/app/bsky/embed/images' import { View as ViewExternal } from '../lexicon/types/app/bsky/embed/external' import { View as ViewRecord } from '../lexicon/types/app/bsky/embed/record' import { View as ViewRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' +import { Label } from '../lexicon/types/com/atproto/label/defs' export type FeedEmbeds = { [uri: string]: ViewImages | ViewExternal | ViewRecord | ViewRecordWithMedia @@ -29,6 +30,7 @@ export type ActorView = { displayName?: string avatar?: string viewer?: { muted?: boolean; following?: string; followedBy?: string } + labels?: Label[] } export type ActorViewMap = { [did: string]: ActorView } diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 3d9822835e9..6197133864c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -43,10 +43,10 @@ export default function (server: Server, ctx: AppContext) { } const relevant = getRelevantIds(threadData) const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester), + feedService.getActorViews(Array.from(relevant.dids), requester, true), feedService.getPostViews(Array.from(relevant.uris), requester), feedService.embedsForPosts(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects(Array.from(relevant.uris)), + labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) const thread = composeThread( diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 0009a5dfb57..dc4d73cb4e5 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { feedService.getActorViews(Array.from(dids), requester), feedService.getPostViews(Array.from(uris), requester), feedService.embedsForPosts(Array.from(uris), requester), - labelService.getLabelsForSubjects(Array.from(uris)), + labelService.getLabelsForUris(Array.from(uris)), ]) const posts: PostView[] = [] diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts index db0188d1929..dd95d2dd80c 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -107,7 +107,7 @@ export default function (server: Server, ctx: AppContext) { })), requester, ), - labelService.getLabelsForSubjects(recordUris), + labelService.getLabelsForUris(recordUris), ]) const bytesByCid = blocks.reduce((acc, block) => { diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index 1d56d9213ce..2d6a19fdee5 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -86,7 +86,7 @@ export class ActorViews { const [profileInfos, labels, listMutes] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForProfiles(dids), + this.services.label(this.db).getLabelsForSubjects(dids), this.getListMutes(dids, viewer), ]) @@ -185,7 +185,7 @@ export class ActorViews { const [profileInfos, labels, listMutes] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForProfiles(dids), + this.services.label(this.db).getLabelsForSubjects(dids), this.getListMutes(dids, viewer), ]) diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index 6ef3e74672a..8226ec517b9 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -117,6 +117,7 @@ export class FeedService { async getActorViews( dids: string[], requester: string, + skipLabels?: boolean, // @NOTE used by hydrateFeed() to batch label hydration ): Promise { if (dids.length < 1) return {} const { ref } = this.db.db.dynamic @@ -164,10 +165,11 @@ export class FeedService { .as('requesterMuted'), ]) .execute(), - this.services.label.getLabelsForProfiles(dids), + this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), this.services.actor.views.getListMutes(dids, requester), ]) return actors.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] return { ...acc, [cur.did]: { @@ -185,7 +187,7 @@ export class FeedService { following: cur?.requesterFollowing || undefined, followedBy: cur?.requesterFollowedBy || undefined, }, - labels: labels[cur.did] ?? [], + labels: skipLabels ? undefined : actorLabels, }, } }, {} as ActorViewMap) @@ -307,7 +309,7 @@ export class FeedService { requester, ), this.embedsForPosts(nestedPostUris, requester, _depth + 1), - this.services.label.getLabelsForSubjects(nestedPostUris), + this.services.label.getLabelsForUris(nestedPostUris), this.getFeedGeneratorViews(nestedFeedGenUris, requester), ]) let embeds = images.reduce((acc, cur) => { @@ -431,10 +433,10 @@ export class FeedService { } } const [actors, posts, embeds, labels] = await Promise.all([ - this.getActorViews(Array.from(actorDids), requester), + this.getActorViews(Array.from(actorDids), requester, true), this.getPostViews(Array.from(postUris), requester), this.embedsForPosts(Array.from(postUris), requester), - this.services.label.getLabelsForSubjects(Array.from(postUris)), + this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) return this.views.formatFeed( diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts index 9b33b92e027..3e76c46bee9 100644 --- a/packages/pds/src/app-view/services/feed/types.ts +++ b/packages/pds/src/app-view/services/feed/types.ts @@ -7,6 +7,7 @@ import { NotFoundPost, PostView, } from '../../../lexicon/types/app/bsky/feed/defs' +import { Label } from '../../../lexicon/types/com/atproto/label/defs' import { FeedGenerator } from '../../db/tables/feed-generator' export type FeedEmbeds = { @@ -40,6 +41,7 @@ export type ActorView = { following?: string followedBy?: string } + labels?: Label[] } export type ActorViewMap = { [did: string]: ActorView } diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 730ef8e9b73..7700995015e 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -122,6 +122,9 @@ export class FeedViews { const post = posts[uri] const author = actors[post?.creator] if (!post || !author) return undefined + // If the author labels are not hydrated yet, attempt to pull them + // from labels: e.g. compatible with hydrateFeed() batching label hydration. + author.labels ??= labels[author.did] ?? [] return { uri: post.uri, cid: post.cid, diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index 81710f8e41b..9de4670c16e 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -60,7 +60,7 @@ export class LabelService { .execute() } - async getLabelsForSubjects( + async getLabelsForUris( subjects: string[], includeNeg?: boolean, ): Promise { @@ -82,28 +82,42 @@ export class LabelService { }, {} as Labels) } - // gets labels for both did & profile record - async getLabelsForProfiles( - dids: string[], + // gets labels for any record. when did is present, combine labels for both did & profile record. + async getLabelsForSubjects( + subjects: string[], includeNeg?: boolean, ): Promise { - if (dids.length < 1) return {} - const profileUris = dids.map((did) => - AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), - ) - const subjects = [...dids, ...profileUris] - const labels = await this.getLabelsForSubjects(subjects, includeNeg) - // combine labels for profile + did + if (subjects.length < 1) return {} + const expandedSubjects = subjects.flatMap((subject) => { + if (subject.startsWith('did:')) { + return [ + subject, + AtUri.make(subject, ids.AppBskyActorProfile, 'self').toString(), + ] + } + return subject + }) + const labels = await this.getLabelsForUris(expandedSubjects, includeNeg) return Object.keys(labels).reduce((acc, cur) => { - const did = cur.startsWith('at://') ? new AtUri(cur).hostname : cur - acc[did] ??= [] - acc[did] = [...acc[did], ...labels[cur]] + const uri = cur.startsWith('at://') ? new AtUri(cur) : null + if ( + uri && + uri.collection === ids.AppBskyActorProfile && + uri.rkey === 'self' + ) { + // combine labels for profile + did + const did = uri.hostname + acc[did] ??= [] + acc[did].push(...labels[cur]) + } + acc[cur] ??= [] + acc[cur].push(...labels[cur]) return acc }, {} as Labels) } async getLabels(subject: string, includeNeg?: boolean): Promise { - const labels = await this.getLabelsForSubjects([subject], includeNeg) + const labels = await this.getLabelsForUris([subject], includeNeg) return labels[subject] ?? [] } @@ -111,7 +125,7 @@ export class LabelService { did: string, includeNeg?: boolean, ): Promise { - const labels = await this.getLabelsForProfiles([did], includeNeg) + const labels = await this.getLabelsForSubjects([did], includeNeg) return labels[did] ?? [] } }