Actor service fix ()

* 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:
Daniel Holmgren 2023-08-01 12:38:14 -05:00 committed by GitHub
parent 3befcfe35e
commit 1ad6274202
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 478 additions and 380 deletions

@ -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,

@ -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,