import { ok } from '@atcute/client'; import type { ResolvedActor } from '@atcute/identity-resolver'; import type { Did, Nsid, ResourceUri } from '@atcute/lexicons/syntax'; import type { OAuthUserAgent } from '@atcute/oauth-browser-client'; import { ShTangledGraphFollow } from '@atcute/tangled'; import { getRpc } from './client'; import { createAuthRpc, resolveActor, resolveActorForDisplay } from './identity'; import { parseAtUri, type RecordCreationResult } from './records'; import { listAllAppviewRecords } from './appview'; const FOLLOW_COLLECTION = 'sh.tangled.graph.follow' as Nsid; export interface FollowRecord { uri: ResourceUri; cid: string; rkey: string; value: ShTangledGraphFollow.Main; } export const listFollowRecords = async (actor: string | ResolvedActor): Promise => { const resolved = typeof actor === 'string' ? await resolveActor(actor) : actor; const rpc = getRpc(resolved.pds); const records: FollowRecord[] = []; let cursor: string | undefined; do { const page = await ok( rpc.get('com.atproto.repo.listRecords', { params: { repo: resolved.did, collection: FOLLOW_COLLECTION, limit: 100, cursor, reverse: true, }, }), ); records.push( ...page.records.map((record) => ({ uri: record.uri, cid: record.cid, rkey: parseAtUri(record.uri).rkey, value: record.value as ShTangledGraphFollow.Main, })), ); cursor = page.cursor; } while (cursor); return records; }; export const createFollow = async (agent: OAuthUserAgent, followeeDid: Did): Promise => { const rpc = createAuthRpc(agent); const record: ShTangledGraphFollow.Main = { $type: 'sh.tangled.graph.follow', subject: followeeDid, createdAt: new Date().toISOString(), }; const result = await ok( rpc.post('com.atproto.repo.createRecord', { input: { repo: agent.sub, collection: FOLLOW_COLLECTION, record, }, }), ); return { uri: result.uri, rkey: parseAtUri(result.uri).rkey, }; }; export const deleteFollow = async (agent: OAuthUserAgent, rkey: string): Promise => { const rpc = createAuthRpc(agent); await ok( rpc.post('com.atproto.repo.deleteRecord', { input: { repo: agent.sub, collection: FOLLOW_COLLECTION, rkey, }, }), ); }; export interface FollowerFollowingItem { did: Did; handle: string; actor: ResolvedActor; followRecord: FollowRecord; } export const getFollowers = async (did: Did): Promise => { const records = await listAllAppviewRecords('sh.tangled.graph.listFollows', did); return Promise.all( records.map(async (record) => { const parsed = parseAtUri(record.uri); const actor = await resolveActorForDisplay(parsed.did); return { did: parsed.did, handle: actor.handle, actor, followRecord: { uri: record.uri, cid: record.cid ?? '', rkey: parsed.rkey, value: record.value, }, }; }) ); }; export const getFollowing = async (did: Did): Promise => { const records = await listAllAppviewRecords('sh.tangled.graph.listFollowsBy', did); return Promise.all( records.map(async (record) => { const targetDid = record.value.subject as Did; const actor = await resolveActorForDisplay(targetDid); const parsed = parseAtUri(record.uri); return { did: targetDid, handle: actor.handle, actor, followRecord: { uri: record.uri, cid: record.cid ?? '', rkey: parsed.rkey, value: record.value, }, }; }) ); }; const VOUCH_COLLECTION = 'sh.tangled.graph.vouch'; export interface VouchRecord { uri: string; cid: string; value: { $type: 'sh.tangled.graph.vouch'; createdAt: string; kind: 'vouch' | 'denounce'; reason?: string; evidences?: string[]; }; } export const getVouchRecord = async ( viewerDid: Did, subjectDid: Did ): Promise => { try { const resolvedViewer = await resolveActor(viewerDid); const rpc = getRpc(resolvedViewer.pds); const result = await ok( rpc.get('com.atproto.repo.getRecord', { params: { repo: resolvedViewer.did, collection: VOUCH_COLLECTION, rkey: subjectDid, }, }) ); return result as unknown as VouchRecord; } catch (e) { return null; } }; export const putVouchRecord = async ( agent: OAuthUserAgent, subjectDid: Did, kind: 'vouch' | 'denounce', reason?: string, evidences?: string[] ): Promise => { const rpc = createAuthRpc(agent); const record = { $type: VOUCH_COLLECTION, createdAt: new Date().toISOString(), kind, reason: reason || undefined, evidences: evidences || undefined, }; await ok( rpc.post('com.atproto.repo.putRecord', { input: { repo: agent.sub, collection: VOUCH_COLLECTION, rkey: subjectDid, record, }, }) ); }; export const deleteVouchRecord = async ( agent: OAuthUserAgent, subjectDid: Did ): Promise => { const rpc = createAuthRpc(agent); await ok( rpc.post('com.atproto.repo.deleteRecord', { input: { repo: agent.sub, collection: VOUCH_COLLECTION, rkey: subjectDid, }, }) ); };