import { ok } from "@atcute/client"; import { mainSchema as createRecordSchema } from "@atcute/atproto/types/repo/createRecord"; 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 type { BobbinContext } from "./client"; import { items } from "./pagination"; import { rkeyFromUri } from "./uri"; import type * as ShTangledGraphFollow from "./lexicons/types/sh/tangled/graph/follow"; import type * as ShTangledGraphVouch from "./lexicons/types/sh/tangled/graph/vouch"; import type * as ShTangledFeedStar from "./lexicons/types/sh/tangled/feed/star"; export type FollowRecord = ShTangledGraphFollow.Main; export type VouchRecord = ShTangledGraphVouch.Main; export type StarRecord = ShTangledFeedStar.Main; const FOLLOW_COLLECTION = "sh.tangled.graph.follow" as Nsid; const STAR_COLLECTION = "sh.tangled.feed.star" as Nsid; // TODO(bobbin): needs a relation point-lookup (e.g. graph.getFollow?actor=&subject=) // or a `viewer` hydration param on lists; scanning listFollowsBy pages is O(follows). export const findFollowRkey = async ( ctx: BobbinContext, viewerDid: string, subject: string, options: { maxPages?: number } = {} ): Promise => { for await (const item of items( ctx, "sh.tangled.graph.listFollowsBy", { subject: viewerDid as Did }, { maxPages: options.maxPages ?? 10 } )) { if ((item.value as FollowRecord).subject === subject) return rkeyFromUri(item.uri); } return null; }; 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 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); // TODO(bobbin): same relation-lookup gap as findFollowRkey; walking every star of // the viewer to build this map is O(stars) per page load. export const listStarRkeys = async ( ctx: BobbinContext, viewerDid: string, options: { maxPages?: number } = {} ): Promise> => { const rkeys = new Map(); for await (const item of items( ctx, "sh.tangled.feed.listStarsBy", { subject: viewerDid as Did }, { maxPages: options.maxPages ?? 10 } )) { const value = item.value as StarRecord; if (value.subject.$type === "sh.tangled.feed.star#repo") { rkeys.set(value.subject.did, rkeyFromUri(item.uri)); } } return rkeys; };