tangled
alpha
login
or
join now
bnewbold.net
/
cobalt
13
fork
atom
go scratch code for atproto
13
fork
atom
overview
issues
pulls
pipelines
flesh out netclient code
bnewbold.net
7 months ago
a497cb67
16e6d100
+198
-4
4 changed files
expand all
collapse all
unified
split
atproto
heap
cid.go
examples_test.go
netclient.go
record.go
+156
atproto/heap/record.go
···
1
1
+
package heap
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"context"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"log/slog"
9
9
+
"net/http"
10
10
+
11
11
+
"github.com/bluesky-social/indigo/atproto/data"
12
12
+
"github.com/bluesky-social/indigo/atproto/repo"
13
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
14
+
)
15
15
+
16
16
+
type repoRecordResp struct {
17
17
+
URI string `json:"uri"`
18
18
+
CID syntax.CID `json:"cid"`
19
19
+
Value json.RawMessage `json:"value"`
20
20
+
}
21
21
+
22
22
+
// Fetches record JSON using com.atproto.repo.getRecord, and returns record as [json.RawMessage] and the CID (as string).
23
23
+
func (nc *NetClient) GetRecordUnverified(ctx context.Context, did syntax.DID, collection syntax.NSID, rkey syntax.RecordKey) (*json.RawMessage, syntax.CID, error) {
24
24
+
ident, err := nc.Dir.LookupDID(ctx, did)
25
25
+
if err != nil {
26
26
+
return nil, "", err
27
27
+
}
28
28
+
host := ident.PDSEndpoint()
29
29
+
if host == "" {
30
30
+
return nil, "", fmt.Errorf("account has no PDS host registered: %s", did.String())
31
31
+
}
32
32
+
// TODO: validate host
33
33
+
// TODO: DID escaping (?)
34
34
+
u := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s", host, did, collection, rkey)
35
35
+
36
36
+
slog.Debug("fetching record JSON", "did", did, "url", u)
37
37
+
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
38
38
+
if err != nil {
39
39
+
return nil, "", err
40
40
+
}
41
41
+
if nc.UserAgent != "" {
42
42
+
req.Header.Set("User-Agent", nc.UserAgent)
43
43
+
}
44
44
+
req.Header.Set("Accept", "application/json")
45
45
+
46
46
+
resp, err := nc.Client.Do(req)
47
47
+
if err != nil {
48
48
+
return nil, "", fmt.Errorf("fetching record JSON (%s): %w", did, err)
49
49
+
}
50
50
+
defer resp.Body.Close()
51
51
+
52
52
+
if resp.StatusCode != http.StatusOK {
53
53
+
return nil, "", fmt.Errorf("HTTP error fetching record JSON (%s): %d", did, resp.StatusCode)
54
54
+
}
55
55
+
56
56
+
var rrr repoRecordResp
57
57
+
if err := json.NewDecoder(resp.Body).Decode(&rrr); err != nil {
58
58
+
return nil, "", fmt.Errorf("failed decoding account status response: %w", err)
59
59
+
}
60
60
+
61
61
+
return &rrr.Value, rrr.CID, nil
62
62
+
}
63
63
+
64
64
+
// Fetches a record "proof" using com.atproto.sync.getRecord. Verifies signature and merkel chain. Copies record content in out 'out' parameter.
65
65
+
//
66
66
+
// If out is nil, record data is not returned. If it is [bytes.Buffer], the record CBOR is copied in. Otherwise, the record is transformed to JSON and Unmarshalled in to provided output, which could be a pointer to a struct, [json.RawMessage], `map[string]any`, etc.
67
67
+
//
68
68
+
// TODO: this might not be fully validating MST tree and record CID hashes or encoding yet
69
69
+
func (nc *NetClient) GetRecord(ctx context.Context, did syntax.DID, collection syntax.NSID, rkey syntax.RecordKey, out any) (syntax.CID, error) {
70
70
+
// TODO: "GetRecordProof" variant, which just returns CAR as io.ReadCloser?
71
71
+
ident, err := nc.Dir.LookupDID(ctx, did)
72
72
+
if err != nil {
73
73
+
return "", err
74
74
+
}
75
75
+
pub, err := ident.PublicKey()
76
76
+
if err != nil {
77
77
+
return "", err
78
78
+
}
79
79
+
host := ident.PDSEndpoint()
80
80
+
if host == "" {
81
81
+
return "", fmt.Errorf("account has no PDS host registered: %s", did.String())
82
82
+
}
83
83
+
// TODO: validate host
84
84
+
// TODO: DID escaping (?)
85
85
+
u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRecord?did=%s&collection=%s&rkey=%s", host, did, collection, rkey)
86
86
+
87
87
+
slog.Debug("fetching record proof", "did", did, "url", u)
88
88
+
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
89
89
+
if err != nil {
90
90
+
return "", err
91
91
+
}
92
92
+
if nc.UserAgent != "" {
93
93
+
req.Header.Set("User-Agent", nc.UserAgent)
94
94
+
}
95
95
+
req.Header.Set("Accept", "application/vnd.ipld.car")
96
96
+
97
97
+
resp, err := nc.Client.Do(req)
98
98
+
if err != nil {
99
99
+
return "", fmt.Errorf("fetching record proof (%s): %w", did, err)
100
100
+
}
101
101
+
defer resp.Body.Close()
102
102
+
103
103
+
if resp.StatusCode != http.StatusOK {
104
104
+
return "", fmt.Errorf("HTTP error fetching record proof (%s): %d", did, resp.StatusCode)
105
105
+
}
106
106
+
107
107
+
// TODO: re-confirm if loading tree re-checks all CIDs; or if we need to re-compute the tree data CID
108
108
+
commit, rp, err := repo.LoadRepoFromCAR(ctx, resp.Body)
109
109
+
if err != nil {
110
110
+
return "", fmt.Errorf("failed to parse record proof CAR (%s): %w", did, err)
111
111
+
}
112
112
+
113
113
+
// NOTE: LoadRepoFromCAR calls commit.VerifyStructure() internally
114
114
+
115
115
+
if err := commit.VerifySignature(pub); err != nil {
116
116
+
return "", fmt.Errorf("failed to verify record proof signature (%s): %w", did, err)
117
117
+
}
118
118
+
119
119
+
rbytes, rcid, err := rp.GetRecordBytes(ctx, collection, rkey)
120
120
+
if err != nil {
121
121
+
return "", fmt.Errorf("failed to read record from proof CAR (%s): %w", did, err)
122
122
+
}
123
123
+
cidStr := syntax.CID(rcid.String())
124
124
+
125
125
+
// TODO: `GetRecordBytes` does not currently verify record CID, but unpacking CAR file should have done that? but need to confirm CAR implementation does this
126
126
+
127
127
+
// check that record CBOR is valid, even if we don't return it
128
128
+
rdata, err := data.UnmarshalCBOR(rbytes)
129
129
+
if err != nil {
130
130
+
return "", fmt.Errorf("failed to parse record CBOR (%s): %w", did, err)
131
131
+
}
132
132
+
133
133
+
switch out := out.(type) {
134
134
+
case nil:
135
135
+
// if output isn't captured, bail out early
136
136
+
return cidStr, nil
137
137
+
case *bytes.Buffer:
138
138
+
// simply copy data over
139
139
+
out.Reset()
140
140
+
_, err := out.Write(rbytes)
141
141
+
if err != nil {
142
142
+
return "", err
143
143
+
}
144
144
+
return cidStr, nil
145
145
+
default:
146
146
+
// attempt to unmarshal from json
147
147
+
jsonBytes, err := json.Marshal(rdata)
148
148
+
if err != nil {
149
149
+
return "", err
150
150
+
}
151
151
+
if err := json.Unmarshal(jsonBytes, out); err != nil {
152
152
+
return "", fmt.Errorf("failed unmarhsaling record (%s): %w", did, err)
153
153
+
}
154
154
+
return cidStr, nil
155
155
+
}
156
156
+
}
+1
-1
atproto/netclient/cid.go
atproto/heap/cid.go
···
1
1
-
package netclient
1
1
+
package heap
2
2
3
3
import (
4
4
"github.com/ipfs/go-cid"
+40
-2
atproto/netclient/examples_test.go
atproto/heap/examples_test.go
···
1
1
-
package netclient
1
1
+
package heap
2
2
3
3
import (
4
4
"bytes"
5
5
"context"
6
6
+
"encoding/json"
6
7
"fmt"
7
8
8
9
"github.com/bluesky-social/indigo/atproto/repo"
···
66
67
}
67
68
68
69
fmt.Printf("active=%t status=%s\n", active, status)
69
69
-
// Output: active=true status=
70
70
+
// active=true status=
71
71
+
}
72
72
+
73
73
+
func ExampleNetClient_GetRecordUnverified() {
74
74
+
75
75
+
ctx := context.Background()
76
76
+
nc := NewNetClient()
77
77
+
did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
78
78
+
collection := syntax.NSID("app.bsky.actor.profile")
79
79
+
rkey := syntax.RecordKey("self")
80
80
+
81
81
+
raw, _, err := nc.GetRecordUnverified(ctx, did, collection, rkey)
82
82
+
if err != nil {
83
83
+
panic("failed to fetch record: " + err.Error())
84
84
+
}
85
85
+
var record map[string]any
86
86
+
_ = json.Unmarshal(*raw, &record)
87
87
+
88
88
+
fmt.Println(record["displayName"])
89
89
+
// AT Protocol Developers
90
90
+
}
91
91
+
92
92
+
func ExampleNetClient_GetRecord() {
93
93
+
94
94
+
ctx := context.Background()
95
95
+
nc := NewNetClient()
96
96
+
did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
97
97
+
collection := syntax.NSID("app.bsky.actor.profile")
98
98
+
rkey := syntax.RecordKey("self")
99
99
+
100
100
+
var record map[string]any
101
101
+
_, err := nc.GetRecord(ctx, did, collection, rkey, &record)
102
102
+
if err != nil {
103
103
+
panic("failed to fetch record: " + err.Error())
104
104
+
}
105
105
+
106
106
+
fmt.Println(record["displayName"])
107
107
+
// Output: AT Protocol Developers
70
108
}
+1
-1
atproto/netclient/netclient.go
atproto/heap/netclient.go
···
1
1
-
package netclient
1
1
+
package heap
2
2
3
3
import (
4
4
"bytes"