Actor service fix (#1414)
* fix up bug in actor service * add in hydrate methods * build fix * couple more fixes * tidy * port to appview * dont build branch * fix issue with items in list * tidy * dont build branch * quick fix
This commit is contained in:
parent
3befcfe35e
commit
1ad6274202
packages
bsky/src
api/app/bsky
actor
feed
graph
notification
services
common-web/src
pds/src/app-view
api/app/bsky
actor
feed
graph
notification
services
@ -23,10 +23,17 @@ export default function (server: Server, ctx: AppContext) {
|
||||
'AccountTakedown',
|
||||
)
|
||||
}
|
||||
const profile = await actorService.views.profileDetailed(
|
||||
actorRes,
|
||||
requester,
|
||||
)
|
||||
if (!profile) {
|
||||
throw new InvalidRequestError('Profile not found')
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: await actorService.views.profileDetailed(actorRes, requester),
|
||||
body: profile,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -15,7 +15,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
profiles: await actorService.views.profileDetailed(
|
||||
profiles: await actorService.views.hydrateProfilesDetailed(
|
||||
actorsRes,
|
||||
requester,
|
||||
),
|
||||
|
@ -55,7 +55,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
cursor: suggestionsRes.at(-1)?.did,
|
||||
actors: await actorService.views.profile(suggestionsRes, viewer),
|
||||
actors: await actorService.views.hydrateProfiles(
|
||||
suggestionsRes,
|
||||
viewer,
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -24,7 +24,9 @@ export default function (server: Server, ctx: AppContext) {
|
||||
: []
|
||||
const keyset = new SearchKeyset(sql``, sql``)
|
||||
|
||||
const actors = await services.actor(db).views.profile(results, requester)
|
||||
const actors = await services
|
||||
.actor(db)
|
||||
.views.hydrateProfiles(results, requester)
|
||||
const filtered = actors.filter(
|
||||
(actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy,
|
||||
)
|
||||
|
@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const actors = await services
|
||||
.actor(db)
|
||||
.views.profileBasic(results, requester)
|
||||
.views.hydrateProfilesBasic(results, requester)
|
||||
|
||||
const filtered = actors.filter(
|
||||
(actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy,
|
||||
|
@ -37,6 +37,9 @@ export default function (server: Server, ctx: AppContext) {
|
||||
feedsQb.execute(),
|
||||
actorService.views.profile(creatorRes, viewer),
|
||||
])
|
||||
if (!creatorProfile) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
const profiles = { [creatorProfile.did]: creatorProfile }
|
||||
|
||||
const feeds = feedsRes.map((row) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { mapDefined } from '@atproto/common'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
@ -37,7 +38,19 @@ export default function (server: Server, ctx: AppContext) {
|
||||
})
|
||||
|
||||
const likesRes = await builder.execute()
|
||||
const actors = await services.actor(db).views.profile(likesRes, requester)
|
||||
const actors = await services
|
||||
.actor(db)
|
||||
.views.profiles(likesRes, requester)
|
||||
|
||||
const likes = mapDefined(likesRes, (row) =>
|
||||
actors[row.did]
|
||||
? {
|
||||
createdAt: row.createdAt,
|
||||
indexedAt: row.indexedAt,
|
||||
actor: actors[row.did],
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
@ -45,11 +58,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
uri,
|
||||
cid,
|
||||
cursor: keyset.packFromResult(likesRes),
|
||||
likes: likesRes.map((row, i) => ({
|
||||
createdAt: row.createdAt,
|
||||
indexedAt: row.indexedAt,
|
||||
actor: actors[i],
|
||||
})),
|
||||
likes,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const repostedByRes = await builder.execute()
|
||||
const repostedBy = await services
|
||||
.actor(db)
|
||||
.views.profile(repostedByRes, requester)
|
||||
.views.hydrateProfiles(repostedByRes, requester)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -33,7 +33,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const blocksRes = await blocksReq.execute()
|
||||
|
||||
const actorService = services.actor(db)
|
||||
const blocks = await actorService.views.profile(blocksRes, requester)
|
||||
const blocks = await actorService.views.hydrateProfiles(
|
||||
blocksRes,
|
||||
requester,
|
||||
)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -37,9 +37,12 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const followersRes = await followersReq.execute()
|
||||
const [followers, subject] = await Promise.all([
|
||||
actorService.views.profile(followersRes, requester),
|
||||
actorService.views.hydrateProfiles(followersRes, requester),
|
||||
actorService.views.profile(subjectRes, requester),
|
||||
])
|
||||
if (!subject) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -37,9 +37,12 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const followsRes = await followsReq.execute()
|
||||
const [follows, subject] = await Promise.all([
|
||||
actorService.views.profile(followsRes, requester),
|
||||
actorService.views.hydrateProfiles(followsRes, requester),
|
||||
actorService.views.profile(creatorRes, requester),
|
||||
])
|
||||
if (!subject) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -2,7 +2,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
import { ProfileView } from '../../../../lexicon/types/app/bsky/actor/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.graph.getList({
|
||||
@ -40,20 +39,17 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const itemsRes = await itemsReq.execute()
|
||||
|
||||
const actorService = services.actor(db)
|
||||
const profiles = await actorService.views.profile(itemsRes, requester)
|
||||
const profilesMap = profiles.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.did]: cur,
|
||||
}),
|
||||
{} as Record<string, ProfileView>,
|
||||
const profiles = await actorService.views.hydrateProfiles(
|
||||
itemsRes,
|
||||
requester,
|
||||
)
|
||||
|
||||
const items = itemsRes.map((item) => ({
|
||||
subject: profilesMap[item.did],
|
||||
}))
|
||||
const items = profiles.map((subject) => ({ subject }))
|
||||
|
||||
const creator = await actorService.views.profile(listRes, requester)
|
||||
if (!creator) {
|
||||
throw new InvalidRequestError(`Actor not found: ${listRes.handle}`)
|
||||
}
|
||||
|
||||
const subject = {
|
||||
uri: listRes.uri,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
import { ProfileView } from '../../../../lexicon/types/app/bsky/actor/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.graph.getListMutes({
|
||||
@ -33,17 +32,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const listsRes = await listsReq.execute()
|
||||
|
||||
const actorService = ctx.services.actor(ctx.db)
|
||||
const profiles = await actorService.views.profile(listsRes, requester)
|
||||
const profilesMap = profiles.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.did]: cur,
|
||||
}),
|
||||
{} as Record<string, ProfileView>,
|
||||
)
|
||||
const profiles = await actorService.views.profiles(listsRes, requester)
|
||||
|
||||
const lists = listsRes.map((row) =>
|
||||
graphService.formatListView(row, profilesMap),
|
||||
graphService.formatListView(row, profiles),
|
||||
)
|
||||
|
||||
return {
|
||||
|
@ -35,6 +35,9 @@ export default function (server: Server, ctx: AppContext) {
|
||||
listsReq.execute(),
|
||||
actorService.views.profile(creatorRes, requester),
|
||||
])
|
||||
if (!creator) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
const profileMap = {
|
||||
[creator.did]: creator,
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
cursor: keyset.packFromResult(mutesRes),
|
||||
mutes: await actorService.views.profile(mutesRes, requester),
|
||||
mutes: await actorService.views.hydrateProfiles(mutesRes, requester),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { jsonStringToLex } from '@atproto/lexicon'
|
||||
import { mapDefined } from '@atproto/common'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
@ -78,7 +79,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const labelService = ctx.services.label(ctx.db)
|
||||
const recordUris = notifs.map((notif) => notif.uri)
|
||||
const [authors, labels] = await Promise.all([
|
||||
actorService.views.profile(
|
||||
actorService.views.profiles(
|
||||
notifs.map((notif) => ({
|
||||
did: notif.authorDid,
|
||||
handle: notif.authorHandle,
|
||||
@ -90,17 +91,21 @@ export default function (server: Server, ctx: AppContext) {
|
||||
labelService.getLabelsForUris(recordUris),
|
||||
])
|
||||
|
||||
const notifications = notifs.map((notif, i) => ({
|
||||
uri: notif.uri,
|
||||
cid: notif.cid,
|
||||
author: authors[i],
|
||||
reason: notif.reason,
|
||||
reasonSubject: notif.reasonSubject || undefined,
|
||||
record: jsonStringToLex(notif.recordJson) as Record<string, unknown>,
|
||||
isRead: seenAt ? notif.indexedAt <= seenAt : false,
|
||||
indexedAt: notif.indexedAt,
|
||||
labels: labels[notif.uri] ?? [],
|
||||
}))
|
||||
const notifications = mapDefined(notifs, (notif) => {
|
||||
const author = authors[notif.authorDid]
|
||||
if (!author) return undefined
|
||||
return {
|
||||
uri: notif.uri,
|
||||
cid: notif.cid,
|
||||
author,
|
||||
reason: notif.reason,
|
||||
reasonSubject: notif.reasonSubject || undefined,
|
||||
record: jsonStringToLex(notif.recordJson) as Record<string, unknown>,
|
||||
isRead: seenAt ? notif.indexedAt <= seenAt : false,
|
||||
indexedAt: notif.indexedAt,
|
||||
labels: labels[notif.uri] ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ArrayEl } from '@atproto/common'
|
||||
import { mapDefined } from '@atproto/common'
|
||||
import { INVALID_HANDLE } from '@atproto/identifier'
|
||||
import {
|
||||
ProfileViewDetailed,
|
||||
@ -10,32 +10,22 @@ import { noMatch, notSoftDeletedClause } from '../../db/util'
|
||||
import { Actor } from '../../db/tables/actor'
|
||||
import { ImageUriBuilder } from '../../image/uri'
|
||||
import { LabelService } from '../label'
|
||||
import { ListViewBasic } from '../../lexicon/types/app/bsky/graph/defs'
|
||||
import { GraphService } from '../graph'
|
||||
|
||||
export class ActorViews {
|
||||
constructor(private db: Database, private imgUriBuilder: ImageUriBuilder) {}
|
||||
|
||||
services = {
|
||||
label: LabelService.creator(),
|
||||
label: LabelService.creator()(this.db),
|
||||
graph: GraphService.creator(this.imgUriBuilder)(this.db),
|
||||
}
|
||||
|
||||
profileDetailed(
|
||||
result: ActorResult,
|
||||
async profilesDetailed(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed>
|
||||
profileDetailed(
|
||||
result: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed[]>
|
||||
async profileDetailed(
|
||||
result: ActorResult | ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed | ProfileViewDetailed[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
): Promise<Record<string, ProfileViewDetailed>> {
|
||||
if (results.length === 0) return {}
|
||||
|
||||
const { ref } = this.db.db.dynamic
|
||||
const { skipLabels = false, includeSoftDeleted = false } = opts ?? {}
|
||||
@ -51,6 +41,7 @@ export class ActorViews {
|
||||
)
|
||||
.select([
|
||||
'actor.did as did',
|
||||
'actor.handle as handle',
|
||||
'profile.uri as profileUri',
|
||||
'profile.displayName as displayName',
|
||||
'profile.description as description',
|
||||
@ -95,79 +86,101 @@ export class ActorViews {
|
||||
.where('mutedByDid', '=', viewer ?? '')
|
||||
.select('subjectDid')
|
||||
.as('requesterMuted'),
|
||||
this.db.db
|
||||
.selectFrom('list_item')
|
||||
.if(!viewer, (q) => q.where(noMatch))
|
||||
.innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri')
|
||||
.where('list_mute.mutedByDid', '=', viewer ?? '')
|
||||
.whereRef('list_item.subjectDid', '=', ref('actor.did'))
|
||||
.select('list_item.listUri')
|
||||
.limit(1)
|
||||
.as('requesterMutedByList'),
|
||||
])
|
||||
|
||||
const [profileInfos, labels, listMutes] = await Promise.all([
|
||||
const [profileInfos, labels] = await Promise.all([
|
||||
profileInfosQb.execute(),
|
||||
this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
this.getListMutes(dids, viewer),
|
||||
this.services.label.getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
])
|
||||
|
||||
const profileInfoByDid = profileInfos.reduce((acc, info) => {
|
||||
return Object.assign(acc, { [info.did]: info })
|
||||
}, {} as Record<string, ArrayEl<typeof profileInfos>>)
|
||||
const listUris: string[] = profileInfos
|
||||
.map((a) => a.requesterMutedByList)
|
||||
.filter((list) => !!list)
|
||||
const listViews = await this.services.graph.getListViews(listUris, viewer)
|
||||
|
||||
const views = results.map((result) => {
|
||||
const profileInfo = profileInfoByDid[result.did]
|
||||
const avatar = profileInfo?.avatarCid
|
||||
return profileInfos.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur?.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'avatar',
|
||||
profileInfo.did,
|
||||
profileInfo.avatarCid,
|
||||
cur.did,
|
||||
cur.avatarCid,
|
||||
)
|
||||
: undefined
|
||||
const banner = profileInfo?.bannerCid
|
||||
const banner = cur?.bannerCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'banner',
|
||||
profileInfo.did,
|
||||
profileInfo.bannerCid,
|
||||
cur.did,
|
||||
cur.bannerCid,
|
||||
)
|
||||
: undefined
|
||||
return {
|
||||
did: result.did,
|
||||
handle: result.handle ?? INVALID_HANDLE,
|
||||
displayName: profileInfo?.displayName || undefined,
|
||||
description: profileInfo?.description || undefined,
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle ?? INVALID_HANDLE,
|
||||
displayName: cur?.displayName || undefined,
|
||||
description: cur?.description || undefined,
|
||||
avatar,
|
||||
banner,
|
||||
followsCount: profileInfo?.followsCount ?? 0,
|
||||
followersCount: profileInfo?.followersCount ?? 0,
|
||||
postsCount: profileInfo?.postsCount ?? 0,
|
||||
indexedAt: profileInfo?.indexedAt || undefined,
|
||||
followsCount: cur?.followsCount ?? 0,
|
||||
followersCount: cur?.followersCount ?? 0,
|
||||
postsCount: cur?.postsCount ?? 0,
|
||||
indexedAt: cur?.indexedAt || undefined,
|
||||
viewer: viewer
|
||||
? {
|
||||
following: profileInfo?.requesterFollowing || undefined,
|
||||
followedBy: profileInfo?.requesterFollowedBy || undefined,
|
||||
muted: !!profileInfo?.requesterMuted || !!listMutes[result.did],
|
||||
mutedByList: listMutes[result.did],
|
||||
blockedBy: !!profileInfo.requesterBlockedBy,
|
||||
blocking: profileInfo.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
followedBy: cur?.requesterFollowedBy || undefined,
|
||||
muted: !!cur?.requesterMuted || !!cur.requesterMutedByList,
|
||||
mutedByList,
|
||||
blockedBy: !!cur.requesterBlockedBy,
|
||||
blocking: cur.requesterBlocking || undefined,
|
||||
}
|
||||
: undefined,
|
||||
labels: labels[result.did] ?? [],
|
||||
labels: skipLabels ? undefined : actorLabels,
|
||||
}
|
||||
})
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileViewDetailed>)
|
||||
}
|
||||
|
||||
profile(
|
||||
async hydrateProfilesDetailed(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed[]> {
|
||||
const profiles = await this.profilesDetailed(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profileDetailed(
|
||||
result: ActorResult,
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView>
|
||||
profile(
|
||||
result: ActorResult[],
|
||||
): Promise<ProfileViewDetailed | null> {
|
||||
const profiles = await this.profilesDetailed([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
|
||||
async profiles(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView[]>
|
||||
async profile(
|
||||
result: ActorResult | ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView | ProfileView[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
): Promise<Record<string, ProfileView>> {
|
||||
if (results.length === 0) return {}
|
||||
|
||||
const { ref } = this.db.db.dynamic
|
||||
const { skipLabels = false, includeSoftDeleted = false } = opts ?? {}
|
||||
@ -182,6 +195,7 @@ export class ActorViews {
|
||||
)
|
||||
.select([
|
||||
'actor.did as did',
|
||||
'actor.handle as handle',
|
||||
'profile.uri as profileUri',
|
||||
'profile.displayName as displayName',
|
||||
'profile.description as description',
|
||||
@ -222,120 +236,121 @@ export class ActorViews {
|
||||
.where('mutedByDid', '=', viewer ?? '')
|
||||
.select('subjectDid')
|
||||
.as('requesterMuted'),
|
||||
this.db.db
|
||||
.selectFrom('list_item')
|
||||
.if(!viewer, (q) => q.where(noMatch))
|
||||
.innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri')
|
||||
.where('list_mute.mutedByDid', '=', viewer ?? '')
|
||||
.whereRef('list_item.subjectDid', '=', ref('actor.did'))
|
||||
.select('list_item.listUri')
|
||||
.limit(1)
|
||||
.as('requesterMutedByList'),
|
||||
])
|
||||
|
||||
const [profileInfos, labels, listMutes] = await Promise.all([
|
||||
const [profileInfos, labels] = await Promise.all([
|
||||
profileInfosQb.execute(),
|
||||
this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
this.getListMutes(dids, viewer),
|
||||
this.services.label.getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
])
|
||||
|
||||
const profileInfoByDid = profileInfos.reduce((acc, info) => {
|
||||
return Object.assign(acc, { [info.did]: info })
|
||||
}, {} as Record<string, ArrayEl<typeof profileInfos>>)
|
||||
const listUris: string[] = profileInfos
|
||||
.map((a) => a.requesterMutedByList)
|
||||
.filter((list) => !!list)
|
||||
const listViews = await this.services.graph.getListViews(listUris, viewer)
|
||||
|
||||
const views = results.map((result) => {
|
||||
const profileInfo = profileInfoByDid[result.did]
|
||||
const avatar = profileInfo?.avatarCid
|
||||
return profileInfos.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur?.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'avatar',
|
||||
profileInfo.did,
|
||||
profileInfo.avatarCid,
|
||||
cur.did,
|
||||
cur.avatarCid,
|
||||
)
|
||||
: undefined
|
||||
return {
|
||||
did: result.did,
|
||||
handle: result.handle ?? INVALID_HANDLE,
|
||||
displayName: profileInfo?.displayName || undefined,
|
||||
description: profileInfo?.description || undefined,
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle ?? INVALID_HANDLE,
|
||||
displayName: cur?.displayName || undefined,
|
||||
description: cur?.description || undefined,
|
||||
avatar,
|
||||
indexedAt: profileInfo?.indexedAt || undefined,
|
||||
indexedAt: cur?.indexedAt || undefined,
|
||||
viewer: viewer
|
||||
? {
|
||||
muted: !!profileInfo?.requesterMuted || !!listMutes[result.did],
|
||||
mutedByList: listMutes[result.did],
|
||||
blockedBy: !!profileInfo.requesterBlockedBy,
|
||||
blocking: profileInfo.requesterBlocking || undefined,
|
||||
following: profileInfo?.requesterFollowing || undefined,
|
||||
followedBy: profileInfo?.requesterFollowedBy || undefined,
|
||||
muted: !!cur?.requesterMuted || !!cur.requesterMutedByList,
|
||||
mutedByList,
|
||||
blockedBy: !!cur.requesterBlockedBy,
|
||||
blocking: cur.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
followedBy: cur?.requesterFollowedBy || undefined,
|
||||
}
|
||||
: undefined,
|
||||
labels: labels[result.did] ?? [],
|
||||
labels: skipLabels ? undefined : actorLabels,
|
||||
}
|
||||
})
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileView>)
|
||||
}
|
||||
|
||||
// @NOTE keep in sync with feedService.getActorViews()
|
||||
profileBasic(
|
||||
async hydrateProfiles(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView[]> {
|
||||
const profiles = await this.profiles(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profile(
|
||||
result: ActorResult,
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic>
|
||||
profileBasic(
|
||||
result: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic[]>
|
||||
async profileBasic(
|
||||
result: ActorResult | ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic | ProfileViewBasic[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
|
||||
const profiles = await this.profile(results, viewer, opts)
|
||||
const views = profiles.map((view) => ({
|
||||
did: view.did,
|
||||
handle: view.handle,
|
||||
displayName: view.displayName,
|
||||
avatar: view.avatar,
|
||||
viewer: view.viewer,
|
||||
}))
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
): Promise<ProfileView | null> {
|
||||
const profiles = await this.profiles([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
|
||||
async getListMutes(
|
||||
subjects: string[],
|
||||
mutedBy: string | null,
|
||||
): Promise<Record<string, ListViewBasic>> {
|
||||
if (mutedBy === null) return {}
|
||||
if (subjects.length < 1) return {}
|
||||
const res = await this.db.db
|
||||
.selectFrom('list_item')
|
||||
.innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri')
|
||||
.innerJoin('list', 'list.uri', 'list_item.listUri')
|
||||
.where('list_mute.mutedByDid', '=', mutedBy)
|
||||
.where('list_item.subjectDid', 'in', subjects)
|
||||
.selectAll('list')
|
||||
.select('list_item.subjectDid as subjectDid')
|
||||
.execute()
|
||||
return res.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.subjectDid]: {
|
||||
uri: cur.uri,
|
||||
cid: cur.cid,
|
||||
name: cur.name,
|
||||
purpose: cur.purpose,
|
||||
avatar: cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'avatar',
|
||||
cur.creator,
|
||||
cur.avatarCid,
|
||||
)
|
||||
: undefined,
|
||||
viewer: {
|
||||
muted: true,
|
||||
},
|
||||
indexedAt: cur.indexedAt,
|
||||
},
|
||||
}),
|
||||
{} as Record<string, ListViewBasic>,
|
||||
)
|
||||
// @NOTE keep in sync with feedService.getActorViews()
|
||||
async profilesBasic(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<Record<string, ProfileViewBasic>> {
|
||||
if (results.length === 0) return {}
|
||||
const profiles = await this.profiles(results, viewer, opts)
|
||||
return Object.values(profiles).reduce((acc, cur) => {
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle,
|
||||
displayName: cur.displayName,
|
||||
avatar: cur.avatar,
|
||||
viewer: cur.viewer,
|
||||
}
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileViewBasic>)
|
||||
}
|
||||
|
||||
async hydrateProfilesBasic(
|
||||
results: ActorResult[],
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic[]> {
|
||||
const profiles = await this.profilesBasic(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profileBasic(
|
||||
result: ActorResult,
|
||||
viewer: string | null,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic | null> {
|
||||
const profiles = await this.profilesBasic([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,27 +191,30 @@ export class FeedService {
|
||||
const listViews = await this.services.graph.getListViews(listUris, viewer)
|
||||
return actors.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'avatar',
|
||||
cur.did,
|
||||
cur.avatarCid,
|
||||
)
|
||||
: undefined
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
return {
|
||||
...acc,
|
||||
[cur.did]: {
|
||||
did: cur.did,
|
||||
handle: cur.handle ?? INVALID_HANDLE,
|
||||
displayName: cur.displayName ?? undefined,
|
||||
avatar: cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'avatar',
|
||||
cur.did,
|
||||
cur.avatarCid,
|
||||
)
|
||||
: undefined,
|
||||
avatar,
|
||||
viewer: viewer
|
||||
? {
|
||||
muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList,
|
||||
mutedByList: cur.requesterMutedByList
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined,
|
||||
mutedByList,
|
||||
blockedBy: !!cur?.requesterBlockedBy,
|
||||
blocking: cur?.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
|
13
packages/common-web/src/arrays.ts
Normal file
13
packages/common-web/src/arrays.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const mapDefined = <T, S>(
|
||||
arr: T[],
|
||||
fn: (obj: T) => S | undefined,
|
||||
): S[] => {
|
||||
const output: S[] = []
|
||||
for (const item of arr) {
|
||||
const val = fn(item)
|
||||
if (val !== undefined) {
|
||||
output.push(val)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export * as check from './check'
|
||||
export * as util from './util'
|
||||
|
||||
export * from './arrays'
|
||||
export * from './async'
|
||||
export * from './util'
|
||||
export * from './tid'
|
||||
|
@ -34,10 +34,17 @@ export default function (server: Server, ctx: AppContext) {
|
||||
'AccountTakedown',
|
||||
)
|
||||
}
|
||||
const profile = await actorService.views.profileDetailed(
|
||||
actorRes,
|
||||
requester,
|
||||
)
|
||||
if (!profile) {
|
||||
throw new InvalidRequestError('Profile not found')
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: await actorService.views.profileDetailed(actorRes, requester),
|
||||
body: profile,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
profiles: await actorService.views.profileDetailed(
|
||||
profiles: await actorService.views.hydrateProfilesDetailed(
|
||||
actorsRes,
|
||||
requester,
|
||||
),
|
||||
|
@ -64,14 +64,13 @@ export default function (server: Server, ctx: AppContext) {
|
||||
}
|
||||
|
||||
const suggestionsRes = await suggestionsQb.execute()
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
cursor: suggestionsRes.at(-1)?.did,
|
||||
actors: await services.appView
|
||||
.actor(ctx.db)
|
||||
.views.profile(suggestionsRes, requester),
|
||||
.views.hydrateProfiles(suggestionsRes, requester),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -52,7 +52,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const actors = await services.appView
|
||||
.actor(db)
|
||||
.views.profile(results, requester)
|
||||
.views.hydrateProfiles(results, requester)
|
||||
|
||||
const filtered = actors.filter(
|
||||
(actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy,
|
||||
|
@ -48,7 +48,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const actors = await services.appView
|
||||
.actor(db)
|
||||
.views.profileBasic(results, requester)
|
||||
.views.hydrateProfilesBasic(results, requester)
|
||||
|
||||
const filtered = actors.filter(
|
||||
(actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy,
|
||||
|
@ -48,6 +48,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
feedsQb.execute(),
|
||||
actorService.views.profile(creatorRes, requester),
|
||||
])
|
||||
if (!creatorProfile) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
|
||||
const profiles = { [creatorProfile.did]: creatorProfile }
|
||||
|
||||
const feeds = feedsRes.map((row) =>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { mapDefined } from '@atproto/common'
|
||||
import { Server } from '../../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../../db/pagination'
|
||||
import AppContext from '../../../../../context'
|
||||
@ -57,7 +58,17 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const likesRes = await builder.execute()
|
||||
const actors = await services.appView
|
||||
.actor(db)
|
||||
.views.profile(likesRes, requester)
|
||||
.views.profiles(likesRes, requester)
|
||||
|
||||
const likes = mapDefined(likesRes, (row) =>
|
||||
actors[row.did]
|
||||
? {
|
||||
createdAt: row.createdAt,
|
||||
indexedAt: row.indexedAt,
|
||||
actor: actors[row.did],
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
@ -65,11 +76,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
uri,
|
||||
cid,
|
||||
cursor: keyset.packFromResult(likesRes),
|
||||
likes: likesRes.map((row, i) => ({
|
||||
createdAt: row.createdAt,
|
||||
indexedAt: row.indexedAt,
|
||||
actor: actors[i],
|
||||
})),
|
||||
likes,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -58,7 +58,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const repostedByRes = await builder.execute()
|
||||
const repostedBy = await services.appView
|
||||
.actor(db)
|
||||
.views.profile(repostedByRes, requester)
|
||||
.views.hydrateProfiles(repostedByRes, requester)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -56,7 +56,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const blocksRes = await blocksReq.execute()
|
||||
|
||||
const actorService = services.appView.actor(db)
|
||||
const blocks = await actorService.views.profile(blocksRes, requester)
|
||||
const blocks = await actorService.views.hydrateProfiles(
|
||||
blocksRes,
|
||||
requester,
|
||||
)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -60,9 +60,12 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const followersRes = await followersReq.execute()
|
||||
const [followers, subject] = await Promise.all([
|
||||
actorService.views.profile(followersRes, requester),
|
||||
actorService.views.hydrateProfiles(followersRes, requester),
|
||||
actorService.views.profile(subjectRes, requester),
|
||||
])
|
||||
if (!subject) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -60,9 +60,12 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
const followsRes = await followsReq.execute()
|
||||
const [follows, subject] = await Promise.all([
|
||||
actorService.views.profile(followsRes, requester),
|
||||
actorService.views.hydrateProfiles(followsRes, requester),
|
||||
actorService.views.profile(creatorRes, requester),
|
||||
])
|
||||
if (!subject) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -2,7 +2,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../../db/pagination'
|
||||
import AppContext from '../../../../../context'
|
||||
import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.graph.getList({
|
||||
@ -51,20 +50,17 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const itemsRes = await itemsReq.execute()
|
||||
|
||||
const actorService = services.appView.actor(db)
|
||||
const profiles = await actorService.views.profile(itemsRes, requester)
|
||||
const profilesMap = profiles.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.did]: cur,
|
||||
}),
|
||||
{} as Record<string, ProfileView>,
|
||||
const profiles = await actorService.views.hydrateProfiles(
|
||||
itemsRes,
|
||||
requester,
|
||||
)
|
||||
|
||||
const items = itemsRes.map((item) => ({
|
||||
subject: profilesMap[item.did],
|
||||
}))
|
||||
const items = profiles.map((subject) => ({ subject }))
|
||||
|
||||
const creator = await actorService.views.profile(listRes, requester)
|
||||
if (!creator) {
|
||||
throw new InvalidRequestError(`Actor not found: ${listRes.handle}`)
|
||||
}
|
||||
|
||||
const subject = {
|
||||
uri: listRes.uri,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Server } from '../../../../../lexicon'
|
||||
import { paginate, TimeCidKeyset } from '../../../../../db/pagination'
|
||||
import AppContext from '../../../../../context'
|
||||
import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.graph.getListMutes({
|
||||
@ -44,17 +43,10 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const listsRes = await listsReq.execute()
|
||||
|
||||
const actorService = ctx.services.appView.actor(ctx.db)
|
||||
const profiles = await actorService.views.profile(listsRes, requester)
|
||||
const profilesMap = profiles.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.did]: cur,
|
||||
}),
|
||||
{} as Record<string, ProfileView>,
|
||||
)
|
||||
const profiles = await actorService.views.profiles(listsRes, requester)
|
||||
|
||||
const lists = listsRes.map((row) =>
|
||||
graphService.formatListView(row, profilesMap),
|
||||
graphService.formatListView(row, profiles),
|
||||
)
|
||||
|
||||
return {
|
||||
|
@ -46,6 +46,9 @@ export default function (server: Server, ctx: AppContext) {
|
||||
listsReq.execute(),
|
||||
actorService.views.profile(creatorRes, requester),
|
||||
])
|
||||
if (!creator) {
|
||||
throw new InvalidRequestError(`Actor not found: ${actor}`)
|
||||
}
|
||||
const profileMap = {
|
||||
[creator.did]: creator,
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
cursor: keyset.packFromResult(mutesRes),
|
||||
mutes: await actorService.views.profile(mutesRes, requester),
|
||||
mutes: await actorService.views.hydrateProfiles(mutesRes, requester),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -112,7 +112,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const recordUris = notifs.map((notif) => notif.uri)
|
||||
const [blocks, authors, labels] = await Promise.all([
|
||||
blocksQb ? blocksQb.execute() : emptyBlocksResult,
|
||||
actorService.views.profile(
|
||||
actorService.views.profiles(
|
||||
notifs.map((notif) => ({
|
||||
did: notif.authorDid,
|
||||
handle: notif.authorHandle,
|
||||
@ -127,13 +127,14 @@ export default function (server: Server, ctx: AppContext) {
|
||||
return acc
|
||||
}, {} as Record<string, Uint8Array>)
|
||||
|
||||
const notifications = notifs.flatMap((notif, i) => {
|
||||
const notifications = common.mapDefined(notifs, (notif) => {
|
||||
const bytes = bytesByCid[notif.cid]
|
||||
if (!bytes) return [] // Filter out
|
||||
const author = authors[notif.authorDid]
|
||||
if (!bytes || !author) return undefined
|
||||
return {
|
||||
uri: notif.uri,
|
||||
cid: notif.cid,
|
||||
author: authors[i],
|
||||
author: authors[notif.authorDid],
|
||||
reason: notif.reason,
|
||||
reasonSubject: notif.reasonSubject || undefined,
|
||||
record: common.cborBytesToRecord(bytes),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ArrayEl } from '@atproto/common'
|
||||
import { mapDefined } from '@atproto/common'
|
||||
import {
|
||||
ProfileViewDetailed,
|
||||
ProfileView,
|
||||
@ -24,23 +24,12 @@ export class ActorViews {
|
||||
graph: GraphService.creator(this.imgUriBuilder)(this.db),
|
||||
}
|
||||
|
||||
profileDetailed(
|
||||
result: ActorResult,
|
||||
async profilesDetailed(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed>
|
||||
profileDetailed(
|
||||
result: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed[]>
|
||||
async profileDetailed(
|
||||
result: ActorResult | ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed | ProfileViewDetailed[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
): Promise<Record<string, ProfileViewDetailed>> {
|
||||
if (results.length === 0) return {}
|
||||
|
||||
const { ref } = this.db.db.dynamic
|
||||
const { skipLabels = false, includeSoftDeleted = false } = opts ?? {}
|
||||
@ -58,6 +47,7 @@ export class ActorViews {
|
||||
)
|
||||
.select([
|
||||
'did_handle.did as did',
|
||||
'did_handle.handle as handle',
|
||||
'profile.uri as profileUri',
|
||||
'profile.displayName as displayName',
|
||||
'profile.description as description',
|
||||
@ -112,72 +102,75 @@ export class ActorViews {
|
||||
this.services.label.getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
])
|
||||
|
||||
const profileInfoByDid = profileInfos.reduce((acc, info) => {
|
||||
return Object.assign(acc, { [info.did]: info })
|
||||
}, {} as Record<string, ArrayEl<typeof profileInfos>>)
|
||||
|
||||
const listUris: string[] = profileInfos
|
||||
.map((a) => a.requesterMutedByList)
|
||||
.filter((list) => !!list)
|
||||
const listViews = await this.services.graph.getListViews(listUris, viewer)
|
||||
|
||||
const views = results.map((result) => {
|
||||
const profileInfo = profileInfoByDid[result.did]
|
||||
const avatar = profileInfo?.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid)
|
||||
return profileInfos.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur?.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
|
||||
: undefined
|
||||
const banner = profileInfo?.bannerCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('banner', profileInfo.bannerCid)
|
||||
const banner = cur?.bannerCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('banner', cur.bannerCid)
|
||||
: undefined
|
||||
return {
|
||||
did: result.did,
|
||||
handle: result.handle,
|
||||
displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined,
|
||||
description: truncateUtf8(profileInfo?.description, 256) || undefined,
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle,
|
||||
displayName: truncateUtf8(cur?.displayName, 64) || undefined,
|
||||
description: truncateUtf8(cur?.description, 256) || undefined,
|
||||
avatar,
|
||||
banner,
|
||||
followsCount: profileInfo?.followsCount || 0,
|
||||
followersCount: profileInfo?.followersCount || 0,
|
||||
postsCount: profileInfo?.postsCount || 0,
|
||||
indexedAt: profileInfo?.indexedAt || undefined,
|
||||
followsCount: cur?.followsCount || 0,
|
||||
followersCount: cur?.followersCount || 0,
|
||||
postsCount: cur?.postsCount || 0,
|
||||
indexedAt: cur?.indexedAt || undefined,
|
||||
viewer: {
|
||||
muted:
|
||||
!!profileInfo?.requesterMuted ||
|
||||
!!profileInfo?.requesterMutedByList,
|
||||
mutedByList: profileInfo.requesterMutedByList
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[profileInfo.requesterMutedByList],
|
||||
)
|
||||
: undefined,
|
||||
blockedBy: !!profileInfo.requesterBlockedBy,
|
||||
blocking: profileInfo.requesterBlocking || undefined,
|
||||
following: profileInfo?.requesterFollowing || undefined,
|
||||
followedBy: profileInfo?.requesterFollowedBy || undefined,
|
||||
muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList,
|
||||
mutedByList,
|
||||
blockedBy: !!cur.requesterBlockedBy,
|
||||
blocking: cur.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
followedBy: cur?.requesterFollowedBy || undefined,
|
||||
},
|
||||
labels: labels[result.did] ?? [],
|
||||
labels: skipLabels ? undefined : actorLabels,
|
||||
}
|
||||
})
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileViewDetailed>)
|
||||
}
|
||||
|
||||
profile(
|
||||
async hydrateProfilesDetailed(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewDetailed[]> {
|
||||
const profiles = await this.profilesDetailed(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profileDetailed(
|
||||
result: ActorResult,
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView>
|
||||
profile(
|
||||
result: ActorResult[],
|
||||
): Promise<ProfileViewDetailed | null> {
|
||||
const profiles = await this.profilesDetailed([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
|
||||
async profiles(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView[]>
|
||||
async profile(
|
||||
result: ActorResult | ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView | ProfileView[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
): Promise<Record<string, ProfileView>> {
|
||||
if (results.length === 0) return {}
|
||||
|
||||
const { ref } = this.db.db.dynamic
|
||||
const { skipLabels = false, includeSoftDeleted = false } = opts ?? {}
|
||||
@ -193,6 +186,7 @@ export class ActorViews {
|
||||
)
|
||||
.select([
|
||||
'did_handle.did as did',
|
||||
'did_handle.handle as handle',
|
||||
'profile.uri as profileUri',
|
||||
'profile.displayName as displayName',
|
||||
'profile.description as description',
|
||||
@ -243,78 +237,100 @@ export class ActorViews {
|
||||
this.services.label.getLabelsForSubjects(skipLabels ? [] : dids),
|
||||
])
|
||||
|
||||
const profileInfoByDid = profileInfos.reduce((acc, info) => {
|
||||
return Object.assign(acc, { [info.did]: info })
|
||||
}, {} as Record<string, ArrayEl<typeof profileInfos>>)
|
||||
|
||||
const listUris: string[] = profileInfos
|
||||
.map((a) => a.requesterMutedByList)
|
||||
.filter((list) => !!list)
|
||||
const listViews = await this.services.graph.getListViews(listUris, viewer)
|
||||
|
||||
const views = results.map((result) => {
|
||||
const profileInfo = profileInfoByDid[result.did]
|
||||
const avatar = profileInfo?.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid)
|
||||
return profileInfos.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
|
||||
: undefined
|
||||
return {
|
||||
did: result.did,
|
||||
handle: result.handle,
|
||||
displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined,
|
||||
description: truncateUtf8(profileInfo?.description, 256) || undefined,
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle,
|
||||
displayName: truncateUtf8(cur?.displayName, 64) || undefined,
|
||||
description: truncateUtf8(cur?.description, 256) || undefined,
|
||||
avatar,
|
||||
indexedAt: profileInfo?.indexedAt || undefined,
|
||||
indexedAt: cur?.indexedAt || undefined,
|
||||
viewer: {
|
||||
muted:
|
||||
!!profileInfo?.requesterMuted ||
|
||||
!!profileInfo?.requesterMutedByList,
|
||||
mutedByList: profileInfo.requesterMutedByList
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[profileInfo.requesterMutedByList],
|
||||
)
|
||||
: undefined,
|
||||
blockedBy: !!profileInfo.requesterBlockedBy,
|
||||
blocking: profileInfo.requesterBlocking || undefined,
|
||||
following: profileInfo?.requesterFollowing || undefined,
|
||||
followedBy: profileInfo?.requesterFollowedBy || undefined,
|
||||
muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList,
|
||||
mutedByList,
|
||||
blockedBy: !!cur.requesterBlockedBy,
|
||||
blocking: cur.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
followedBy: cur?.requesterFollowedBy || undefined,
|
||||
},
|
||||
labels: labels[result.did] ?? [],
|
||||
labels: skipLabels ? undefined : actorLabels,
|
||||
}
|
||||
})
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileView>)
|
||||
}
|
||||
|
||||
// @NOTE keep in sync with feedService.getActorViews()
|
||||
profileBasic(
|
||||
async hydrateProfiles(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileView[]> {
|
||||
const profiles = await this.profiles(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profile(
|
||||
result: ActorResult,
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic>
|
||||
profileBasic(
|
||||
result: ActorResult[],
|
||||
): Promise<ProfileView | null> {
|
||||
const profiles = await this.profiles([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
|
||||
// @NOTE keep in sync with feedService.getActorViews()
|
||||
async profilesBasic(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic[]>
|
||||
): Promise<Record<string, ProfileViewBasic>> {
|
||||
if (results.length === 0) return {}
|
||||
const profiles = await this.profiles(results, viewer, opts)
|
||||
return Object.values(profiles).reduce((acc, cur) => {
|
||||
const profile = {
|
||||
did: cur.did,
|
||||
handle: cur.handle,
|
||||
displayName: truncateUtf8(cur.displayName, 64) || undefined,
|
||||
avatar: cur.avatar,
|
||||
viewer: cur.viewer,
|
||||
labels: cur.labels,
|
||||
}
|
||||
acc[cur.did] = profile
|
||||
return acc
|
||||
}, {} as Record<string, ProfileViewBasic>)
|
||||
}
|
||||
|
||||
async hydrateProfilesBasic(
|
||||
results: ActorResult[],
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic[]> {
|
||||
const profiles = await this.profilesBasic(results, viewer, opts)
|
||||
return mapDefined(results, (result) => profiles[result.did])
|
||||
}
|
||||
|
||||
async profileBasic(
|
||||
result: ActorResult | ActorResult[],
|
||||
result: ActorResult,
|
||||
viewer: string,
|
||||
opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean },
|
||||
): Promise<ProfileViewBasic | ProfileViewBasic[]> {
|
||||
const results = Array.isArray(result) ? result : [result]
|
||||
if (results.length === 0) return []
|
||||
|
||||
const profiles = await this.profile(results, viewer, opts)
|
||||
const views = profiles.map((view) => ({
|
||||
did: view.did,
|
||||
handle: view.handle,
|
||||
displayName: truncateUtf8(view.displayName, 64) || undefined,
|
||||
avatar: view.avatar,
|
||||
viewer: view.viewer,
|
||||
labels: view.labels,
|
||||
}))
|
||||
|
||||
return Array.isArray(result) ? views : views[0]
|
||||
): Promise<ProfileViewBasic | null> {
|
||||
const profiles = await this.profilesBasic([result], viewer, opts)
|
||||
return profiles[result.did] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,22 +192,25 @@ export class FeedService {
|
||||
)
|
||||
return actors.reduce((acc, cur) => {
|
||||
const actorLabels = labels[cur.did] ?? []
|
||||
const avatar = cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
|
||||
: undefined
|
||||
const mutedByList =
|
||||
cur.requesterMutedByList && listViews[cur.requesterMutedByList]
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined
|
||||
return {
|
||||
...acc,
|
||||
[cur.did]: {
|
||||
did: cur.did,
|
||||
handle: cur.handle,
|
||||
displayName: truncateUtf8(cur.displayName, 64) || undefined,
|
||||
avatar: cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
|
||||
: undefined,
|
||||
avatar,
|
||||
viewer: {
|
||||
muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList,
|
||||
mutedByList: cur.requesterMutedByList
|
||||
? this.services.graph.formatListViewBasic(
|
||||
listViews[cur.requesterMutedByList],
|
||||
)
|
||||
: undefined,
|
||||
mutedByList,
|
||||
blockedBy: !!cur?.requesterBlockedBy,
|
||||
blocking: cur?.requesterBlocking || undefined,
|
||||
following: cur?.requesterFollowing || undefined,
|
||||
|
Loading…
x
Reference in New Issue
Block a user