Monorepo for Tangled
tangled.org
1package keys
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7
8 comatproto "github.com/bluesky-social/indigo/api/atproto"
9 "github.com/bluesky-social/indigo/atproto/identity"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/knotserver/db"
14 "tangled.org/core/log"
15)
16
17const (
18 publicKeyPageSize = 100
19 maxPublicKeyPages = 20
20)
21
22func FetchAndStore(ctx context.Context, dir identity.Directory, store *db.DB, did syntax.DID) error {
23 l := log.FromContext(ctx)
24
25 id, err := dir.LookupDID(ctx, did)
26 if err != nil {
27 return fmt.Errorf("lookup did to fetch keys: %w", err)
28 }
29
30 serviceEndpoint, ok := id.Services["atproto_pds"]
31 if !ok {
32 l.Warn("did identity did not contain atproto_pds service while adding their keys", "did", did)
33 return nil
34 }
35
36 xrpcc := indigoxrpc.Client{Host: serviceEndpoint.URL}
37 records, err := listAllPublicKeys(ctx, l, &xrpcc, did, "", maxPublicKeyPages)
38 if err != nil {
39 return fmt.Errorf("fetching public keys for did: %w", err)
40 }
41
42 keys := collectPublicKeys(l, did, records)
43 if len(keys) == 0 {
44 l.Warn("no public keys fetched, skipping replace so existing keys are not wiped by a transient empty response", "did", did)
45 return nil
46 }
47
48 if err := store.ReplacePublicKeys(did, keys); err != nil {
49 return fmt.Errorf("replacing public keys in db: %w", err)
50 }
51 return nil
52}
53
54func listAllPublicKeys(ctx context.Context, l *slog.Logger, xrpcc *indigoxrpc.Client, did syntax.DID, cursor string, pagesLeft int) ([]*comatproto.RepoListRecords_Record, error) {
55 if pagesLeft <= 0 {
56 l.Warn("public key pagination hit page cap, remaining keys ignored", "did", did, "cap", maxPublicKeyPages)
57 return nil, nil
58 }
59
60 resp, err := comatproto.RepoListRecords(ctx, xrpcc, tangled.PublicKeyNSID, cursor, publicKeyPageSize, did.String(), false)
61 if err != nil {
62 return nil, err
63 }
64
65 if resp.Cursor == nil || *resp.Cursor == "" || len(resp.Records) == 0 {
66 return resp.Records, nil
67 }
68
69 rest, err := listAllPublicKeys(ctx, l, xrpcc, did, *resp.Cursor, pagesLeft-1)
70 if err != nil {
71 return nil, err
72 }
73
74 return append(resp.Records, rest...), nil
75}
76
77func collectPublicKeys(l *slog.Logger, did syntax.DID, records []*comatproto.RepoListRecords_Record) []db.PublicKey {
78 return collectPublicKeysInto(l, did, records, nil)
79}
80
81func collectPublicKeysInto(l *slog.Logger, did syntax.DID, records []*comatproto.RepoListRecords_Record, acc []db.PublicKey) []db.PublicKey {
82 if len(records) == 0 {
83 return acc
84 }
85
86 return collectPublicKeysInto(l, did, records[1:], appendValidKey(l, did, acc, records[0]))
87}
88
89func appendValidKey(l *slog.Logger, did syntax.DID, acc []db.PublicKey, record *comatproto.RepoListRecords_Record) []db.PublicKey {
90 if record == nil {
91 return acc
92 }
93
94 key, ok := record.Value.Val.(*tangled.PublicKey)
95 if !ok || key == nil {
96 return acc
97 }
98
99 rkey, err := recordKeyFromURI(record.Uri)
100 if err != nil {
101 l.Warn("skipping public key with unparseable uri", "uri", record.Uri, "err", err)
102 return acc
103 }
104
105 return append(acc, db.PublicKey{
106 Did: did,
107 Rkey: rkey,
108 PublicKey: *key,
109 })
110}
111
112func recordKeyFromURI(uri string) (syntax.RecordKey, error) {
113 aturi, err := syntax.ParseATURI(uri)
114 if err != nil {
115 return "", err
116 }
117 return aturi.RecordKey(), nil
118}