import type { Did, Nsid, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { ClientResponseError, type BobbinContext } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; import { httpStatusFor } from "$lib/api/load"; import { didFromUri, rkeyFromUri } from "$lib/api/uri"; import { createRecord, deleteRecord, putRecord, type WrittenRecord } from "$lib/api/write"; import type { VouchRecord } from "$lib/api/records"; import type * as ShTangledFeedGetStar from "$lib/api/lexicons/types/sh/tangled/feed/getStar"; import type * as ShTangledGraphFollow from "$lib/api/lexicons/types/sh/tangled/graph/follow"; import type * as ShTangledGraphGetFollow from "$lib/api/lexicons/types/sh/tangled/graph/getFollow"; import type * as ShTangledGraphListNetworkVouches from "$lib/api/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; const SUBSCRIPTION_COLLECTION = "org.tangled.feed.subscription" as Nsid; 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; } }; 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; } }; export const createFollow = async ( agent: OAuthUserAgent, subject: string ): Promise => { const written = await createRecord(agent, FOLLOW_COLLECTION, { $type: "sh.tangled.graph.follow", subject: subject as Did, createdAt: new Date().toISOString() }); return { ...written, rkey: rkeyFromUri(written.uri) }; }; export const deleteFollow = async ( agent: OAuthUserAgent, rkey: string ): Promise<{ uri: ResourceUri }> => { const uri = `at://${agent.sub}/${FOLLOW_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, FOLLOW_COLLECTION, rkey); return { uri }; }; export type StarSubject = | { kind: "repo"; did: string; ownerHandle: string; name: string } | { kind: "string"; uri: string }; export const createStar = async ( agent: OAuthUserAgent, subject: StarSubject ): Promise => { const record = subject.kind === "repo" ? { $type: "sh.tangled.feed.star#repo" as const, did: subject.did as Did } : { $type: "sh.tangled.feed.star#string" as const, uri: subject.uri as ResourceUri }; const written = await createRecord(agent, STAR_COLLECTION, { $type: "sh.tangled.feed.star", subject: record, createdAt: new Date().toISOString() }); return { ...written, rkey: rkeyFromUri(written.uri) }; }; export const deleteStar = async ( agent: OAuthUserAgent, rkey: string ): Promise<{ uri: ResourceUri }> => { const uri = `at://${agent.sub}/${STAR_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, STAR_COLLECTION, rkey); return { uri }; }; export const createSubscription = async ( agent: OAuthUserAgent, repoDid: string ): Promise => { const written = await createRecord(agent, SUBSCRIPTION_COLLECTION, { $type: SUBSCRIPTION_COLLECTION, subject: { $type: "org.tangled.feed.subscription#repo", did: repoDid as Did }, createdAt: new Date().toISOString() }); return { ...written, rkey: rkeyFromUri(written.uri) }; }; export const deleteSubscription = async ( agent: OAuthUserAgent, rkey: string ): Promise<{ uri: ResourceUri }> => { const uri = `at://${agent.sub}/${SUBSCRIPTION_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, SUBSCRIPTION_COLLECTION, rkey); return { uri }; }; export const createEntitySubscription = async ( agent: OAuthUserAgent, entityUri: string ): Promise => { const { uri } = await createRecord(agent, SUBSCRIPTION_COLLECTION, { $type: SUBSCRIPTION_COLLECTION, subject: { $type: "org.tangled.feed.subscription#uri", uri: entityUri }, createdAt: new Date().toISOString() }); return rkeyFromUri(uri); }; export const getSubscriptionRkey = async ( ctx: BobbinContext, actor: string, subject: string ): Promise => { try { const { uri } = await jsonGet<{ uri: string }>( ctx, "org.tangled.feed.subscription.getForActor", { actor: actor as Did, subject } ); return rkeyFromUri(uri); } catch (cause) { if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; throw cause; } }; export const ensureEntitySubscription = async ( agent: OAuthUserAgent, ctx: BobbinContext, actor: string, entityUri: string ): Promise => { const existing = await getSubscriptionRkey(ctx, actor, entityUri); return existing ?? (await createEntitySubscription(agent, entityUri)); }; export type VouchKind = "vouch" | "denounce"; export interface VouchInput { kind: VouchKind; reason?: string; evidences?: string[]; } export const putVouch = async ( agent: OAuthUserAgent, subject: string, input: VouchInput ): Promise => { return putRecord(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 = async ( agent: OAuthUserAgent, subject: string ): Promise<{ uri: ResourceUri }> => { const uri = `at://${agent.sub}/${VOUCH_COLLECTION}/${subject}` as ResourceUri; await deleteRecord(agent, VOUCH_COLLECTION, subject); return { uri }; }; 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 }; };