import { ok } from "@atcute/client"; import { mainSchema as createRecordSchema } from "@atcute/atproto/types/repo/createRecord"; import { mainSchema as putRecordSchema } from "@atcute/atproto/types/repo/putRecord"; import { mainSchema as deleteRecordSchema } from "@atcute/atproto/types/repo/deleteRecord"; import type { Did, Nsid, RecordKey } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { createClient } from "$lib/auth/agent"; import { ClientResponseError, type BobbinContext } from "./client"; import { jsonGet } from "./_request"; import { httpStatusFor } from "./load"; import { didFromUri, rkeyFromUri } from "./uri"; import type { VouchRecord } from "./records"; import type * as ShTangledFeedGetStar from "./lexicons/types/sh/tangled/feed/getStar"; import type * as ShTangledGraphFollow from "./lexicons/types/sh/tangled/graph/follow"; import type * as ShTangledGraphGetFollow from "./lexicons/types/sh/tangled/graph/getFollow"; import type * as ShTangledGraphListNetworkVouches from "./lexicons/types/sh/tangled/graph/listNetworkVouches"; export type FollowRecord = ShTangledGraphFollow.Main; export type { VouchRecord }; const FOLLOW_COLLECTION = "sh.tangled.graph.follow" as Nsid; const STAR_COLLECTION = "sh.tangled.feed.star" as Nsid; const VOUCH_COLLECTION = "sh.tangled.graph.vouch" as Nsid; // 404 means no follow exists, other errors propagate export const getFollowRkey = async ( ctx: BobbinContext, actor: string, subject: string ): Promise => { try { const { uri } = await jsonGet( ctx, "sh.tangled.graph.getFollow", { actor: actor as Did, subject: subject as Did } satisfies ShTangledGraphGetFollow.$params ); return rkeyFromUri(uri); } catch (cause) { if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; throw cause; } }; // 404 means no star exists, other errors propagate export const getStarRkey = async ( ctx: BobbinContext, actor: string, subject: string ): Promise => { try { const { uri } = await jsonGet(ctx, "sh.tangled.feed.getStar", { actor: actor as Did, subject } satisfies ShTangledFeedGetStar.$params); return rkeyFromUri(uri); } catch (cause) { if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; throw cause; } }; const createGenericRecord = async ( agent: OAuthUserAgent, collection: Nsid, record: T ): Promise => { const rpc = createClient(agent); const result = await ok( rpc.call(createRecordSchema, { input: { repo: agent.sub, collection, record } }) ); return rkeyFromUri(result.uri); }; const putGenericRecord = async ( agent: OAuthUserAgent, collection: Nsid, rkey: string, record: T ): Promise => { const rpc = createClient(agent); const result = await ok( rpc.call(putRecordSchema, { input: { repo: agent.sub, collection, rkey: rkey as RecordKey, record } }) ); return rkeyFromUri(result.uri); }; const deleteGenericRecord = async ( agent: OAuthUserAgent, collection: Nsid, rkey: string ): Promise => { const rpc = createClient(agent); await ok( rpc.call(deleteRecordSchema, { input: { repo: agent.sub, collection, rkey: rkey as RecordKey } }) ); }; export const createFollow = (agent: OAuthUserAgent, subject: string): Promise => createGenericRecord(agent, FOLLOW_COLLECTION, { $type: "sh.tangled.graph.follow", subject: subject as Did, createdAt: new Date().toISOString() }); export const deleteFollow = (agent: OAuthUserAgent, rkey: string): Promise => deleteGenericRecord(agent, FOLLOW_COLLECTION, rkey); export const createStar = (agent: OAuthUserAgent, repoDid: string): Promise => createGenericRecord(agent, STAR_COLLECTION, { $type: "sh.tangled.feed.star", subject: { $type: "sh.tangled.feed.star#repo", did: repoDid as Did }, createdAt: new Date().toISOString() }); export const deleteStar = (agent: OAuthUserAgent, rkey: string): Promise => deleteGenericRecord(agent, STAR_COLLECTION, rkey); export type VouchKind = "vouch" | "denounce"; export interface VouchInput { kind: VouchKind; reason?: string; evidences?: string[]; } export const putVouch = ( agent: OAuthUserAgent, subject: string, input: VouchInput ): Promise => putGenericRecord(agent, VOUCH_COLLECTION, subject, { $type: "sh.tangled.graph.vouch", kind: input.kind, ...(input.reason ? { reason: input.reason } : {}), ...(input.evidences?.length ? { evidences: input.evidences } : {}), createdAt: new Date().toISOString() }); export const deleteVouch = (agent: OAuthUserAgent, subject: string): Promise => deleteGenericRecord(agent, VOUCH_COLLECTION, subject); export const getViewerVouch = async ( ctx: BobbinContext, viewer: string, subject: string ): Promise => { const { items } = await jsonGet( ctx, "sh.tangled.graph.listNetworkVouches", { actor: viewer as Did, limit: 100 } satisfies ShTangledGraphListNetworkVouches.$params ); const match = items.find( (item) => didFromUri(item.uri) === viewer && rkeyFromUri(item.uri) === subject ); if (!match) return null; const value = match.value as VouchRecord; return { kind: value.kind === "denounce" ? "denounce" : "vouch", reason: value.reason }; };