This repository has no description
1import { ok } from '@atcute/client';
2import { mainSchema as createRecordSchema } from '@atcute/atproto/types/repo/createRecord';
3import { mainSchema as deleteRecordSchema } from '@atcute/atproto/types/repo/deleteRecord';
4import type { Did, Nsid, RecordKey } from '@atcute/lexicons/syntax';
5import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
6import { createClient } from '$lib/auth/agent';
7import type { BobbinContext } from './client';
8import { items } from './pagination';
9import { rkeyFromUri } from './uri';
10import type * as ShTangledGraphFollow from './lexicons/types/sh/tangled/graph/follow';
11import type * as ShTangledGraphVouch from './lexicons/types/sh/tangled/graph/vouch';
12import type * as ShTangledFeedStar from './lexicons/types/sh/tangled/feed/star';
13
14export type FollowRecord = ShTangledGraphFollow.Main;
15export type VouchRecord = ShTangledGraphVouch.Main;
16export type StarRecord = ShTangledFeedStar.Main;
17
18const FOLLOW_COLLECTION = 'sh.tangled.graph.follow' as Nsid;
19const STAR_COLLECTION = 'sh.tangled.feed.star' as Nsid;
20
21// TODO(bobbin): needs a relation point-lookup (e.g. graph.getFollow?actor=&subject=)
22// or a `viewer` hydration param on lists; scanning listFollowsBy pages is O(follows).
23export const findFollowRkey = async (
24 ctx: BobbinContext,
25 viewerDid: string,
26 subject: string,
27 options: { maxPages?: number } = {}
28): Promise<string | null> => {
29 for await (const item of items(
30 ctx,
31 'sh.tangled.graph.listFollowsBy',
32 { subject: viewerDid as Did },
33 { maxPages: options.maxPages ?? 10 }
34 )) {
35 if ((item.value as FollowRecord).subject === subject) return rkeyFromUri(item.uri);
36 }
37 return null;
38};
39
40const createGenericRecord = async <T extends { $type: string }>(
41 agent: OAuthUserAgent,
42 collection: Nsid,
43 record: T
44): Promise<string> => {
45 const rpc = createClient(agent);
46 const result = await ok(
47 rpc.call(createRecordSchema, {
48 input: { repo: agent.sub, collection, record }
49 })
50 );
51 return rkeyFromUri(result.uri);
52};
53
54const deleteGenericRecord = async (
55 agent: OAuthUserAgent,
56 collection: Nsid,
57 rkey: string
58): Promise<void> => {
59 const rpc = createClient(agent);
60 await ok(
61 rpc.call(deleteRecordSchema, {
62 input: { repo: agent.sub, collection, rkey: rkey as RecordKey }
63 })
64 );
65};
66
67export const createFollow = (agent: OAuthUserAgent, subject: string): Promise<string> =>
68 createGenericRecord(agent, FOLLOW_COLLECTION, {
69 $type: 'sh.tangled.graph.follow',
70 subject: subject as Did,
71 createdAt: new Date().toISOString()
72 });
73
74export const deleteFollow = (agent: OAuthUserAgent, rkey: string): Promise<void> =>
75 deleteGenericRecord(agent, FOLLOW_COLLECTION, rkey);
76
77export const createStar = (agent: OAuthUserAgent, repoDid: string): Promise<string> =>
78 createGenericRecord(agent, STAR_COLLECTION, {
79 $type: 'sh.tangled.feed.star',
80 subject: {
81 $type: 'sh.tangled.feed.star#repo',
82 did: repoDid as Did
83 },
84 createdAt: new Date().toISOString()
85 });
86
87export const deleteStar = (agent: OAuthUserAgent, rkey: string): Promise<void> =>
88 deleteGenericRecord(agent, STAR_COLLECTION, rkey);
89
90// TODO(bobbin): same relation-lookup gap as findFollowRkey; walking every star of
91// the viewer to build this map is O(stars) per page load.
92export const listStarRkeys = async (
93 ctx: BobbinContext,
94 viewerDid: string,
95 options: { maxPages?: number } = {}
96): Promise<Map<string, string>> => {
97 const rkeys = new Map<string, string>();
98 for await (const item of items(
99 ctx,
100 'sh.tangled.feed.listStarsBy',
101 { subject: viewerDid as Did },
102 { maxPages: options.maxPages ?? 10 }
103 )) {
104 const value = item.value as StarRecord;
105 if (value.subject.$type === 'sh.tangled.feed.star#repo') {
106 rkeys.set(value.subject.did, rkeyFromUri(item.uri));
107 }
108 }
109 return rkeys;
110};