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<FollowRecord[]> => {
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<RecordCreationResult> => {
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<void> => {
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<FollowerFollowingItem[]> => {
const records = await listAllAppviewRecords<ShTangledGraphFollow.Main>('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<FollowerFollowingItem[]> => {
const records = await listAllAppviewRecords<ShTangledGraphFollow.Main>('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<VouchRecord | null> => {
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<void> => {
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<void> => {
const rpc = createAuthRpc(agent);
await ok(
rpc.post('com.atproto.repo.deleteRecord', {
input: {
repo: agent.sub,
collection: VOUCH_COLLECTION,
rkey: subjectDid,
},
})
);
};