import { AppBskyGraphList, AppBskyGraphListitem } from '@atcute/bluesky' import { queryOptions } from '@tanstack/react-query' import type { ActorIdentity, ListSummary, Page, RawRecord, UnavailableItem } from '../../types' import { CACHE_TTL_MS } from '../runtime/cachePolicy' import { tryParseRecordUri } from '../../lib/atproto/uri' import type { PublicDataCore } from '../publicDataCore' import { actorReference, isUnavailableRecord, unavailable } from '../recordResults' import type { DataRequestContext } from '../runtime/sharedQueryRequests' import { countVerifiedBacklinkPublishers } from './verifiedBacklinks' import { queryKeys } from '../queryKeys' import { parsedRecord } from '../recordValues' const RELATIONSHIP_PAGE_SIZE = 24 const FOREIGN_MEMBERSHIP_REASON = 'This list membership was not published by the list owner.' /** * Anyone can publish a listitem naming any list, so a membership counts only when it lives in the list owner's * repository. A valid CID proves the record's content, not the publisher's authority over the list. */ function isPublishedByListOwner(membershipUri: string, listUri: string): boolean { const membership = tryParseRecordUri(membershipUri) const list = tryParseRecordUri(listUri) return ( membership?.collection === 'app.bsky.graph.listitem' && list?.collection === 'app.bsky.graph.list' && membership.repo === list.repo ) } export class ListDataService { constructor( private readonly core: PublicDataCore, private readonly context: Pick, ) {} listBlockCountQueryOptions(listUri?: string) { return { queryKey: queryKeys.listBlockCount(listUri), queryFn: ({ signal }: { signal: AbortSignal }) => listUri ? this.loadListBlockCount(listUri, signal) : Promise.resolve(undefined), enabled: listUri !== undefined, staleTime: CACHE_TTL_MS.activity, } } async listBlockCount(listUri: string, signal?: AbortSignal): Promise { const { queryKey, staleTime } = this.listBlockCountQueryOptions(listUri) return this.context.sharedRequests.query({ queryKey, staleTime, load: (requestSignal) => this.loadListBlockCount(listUri, requestSignal), consumerSignal: signal, requestTimeoutMs: null, }) } private loadListBlockCount(listUri: string, signal: AbortSignal) { return countVerifiedBacklinkPublishers( this.core, { subject: listUri, collection: 'app.bsky.graph.listblock' }, signal, ) } private listFromRecord(record: RawRecord): ListSummary | UnavailableItem { const parts = tryParseRecordUri(record.uri) const value = parsedRecord(AppBskyGraphList.mainSchema, record) if (!parts || !value) return unavailable(record.uri, 'This list definition is malformed or unavailable.') return { kind: 'list', uri: record.uri, name: value.name, description: value.description, purpose: value.purpose, avatarCid: value.avatar?.ref.$link, createdAt: value.createdAt, owner: actorReference(parts.repo), } } listSummaryQueryOptions(uri: string | undefined) { return queryOptions({ queryKey: queryKeys.listSummary(uri), queryFn: ({ signal }) => { if (!uri) throw new Error('This list address is invalid.') return this.core.record(uri, signal).then((record) => this.listFromRecord(record)) }, staleTime: CACHE_TTL_MS.record, }) } async lists( identity: ActorIdentity, cursor?: string, signal?: AbortSignal, ): Promise> { const page = await this.core.repositoryRecords({ identity, collection: 'app.bsky.graph.list', cursor, limit: 25, signal, }) return { items: page.items.map((record) => (isUnavailableRecord(record) ? record : this.listFromRecord(record))), cursor: page.cursor, } } async listMembers(listUri: string, cursor?: string, signal?: AbortSignal): Promise> { const listRecord = tryParseRecordUri(listUri) if (listRecord?.collection !== 'app.bsky.graph.list') { throw new Error('This list address is invalid.') } return this.core.backlinks({ subject: listUri, source: 'app.bsky.graph.listitem:list', did: listRecord.repo, limit: RELATIONSHIP_PAGE_SIZE, cursor, signal, }) } listMemberQueryOptions(listUri: string, membershipUri: string) { return queryOptions({ queryKey: queryKeys.listMember(listUri, membershipUri), queryFn: async ({ signal }) => { if (!isPublishedByListOwner(membershipUri, listUri)) return unavailable(membershipUri, FOREIGN_MEMBERSHIP_REASON) const membership = await this.core.record(membershipUri, signal) const value = parsedRecord(AppBskyGraphListitem.mainSchema, membership) if (!value || value.list !== listUri) return unavailable(membershipUri, 'This list membership is malformed.') return { kind: 'relationship' as const, id: membershipUri, actor: actorReference(value.subject), createdAt: value.createdAt, } }, staleTime: CACHE_TTL_MS.activity, }) } async listedOnReferences(did: string, cursor?: string, signal?: AbortSignal): Promise> { return this.core.backlinks({ subject: did, source: 'app.bsky.graph.listitem:subject', limit: RELATIONSHIP_PAGE_SIZE, cursor, signal, }) } listedOnMembershipQueryOptions(membershipUri: string, subjectDid: ActorIdentity['did'], minimumBlocking?: number) { return queryOptions({ queryKey: queryKeys.listedOnMembership(membershipUri, subjectDid, minimumBlocking), queryFn: async ({ signal }) => { const membership = await this.core.record(membershipUri, signal) const value = parsedRecord(AppBskyGraphListitem.mainSchema, membership) if (!value || value.subject !== subjectDid) return unavailable(membershipUri, 'This list membership is malformed.') if (tryParseRecordUri(value.list)?.collection !== 'app.bsky.graph.list') return unavailable(membershipUri, 'This membership has no valid list reference.') if (!isPublishedByListOwner(membershipUri, value.list)) return unavailable(membershipUri, FOREIGN_MEMBERSHIP_REASON) const list = this.listFromRecord(await this.core.record(value.list, signal)) if (minimumBlocking !== undefined && list.kind === 'list' && list.purpose === 'app.bsky.graph.defs#modlist') { const count = await this.listBlockCount(list.uri, signal) if (count < minimumBlocking) return null } return { kind: 'membership' as const, uri: membershipUri, createdAt: value.createdAt, list } }, staleTime: CACHE_TTL_MS.activity, }) } }