1package plc
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/base32"
8 "encoding/base64"
9 "encoding/json"
10 "fmt"
11 "io"
12 "net/http"
13 "net/url"
14 "strings"
15 "time"
16
17 "github.com/bluesky-social/indigo/atproto/crypto"
18 "github.com/bluesky-social/indigo/atproto/data"
19 "github.com/bluesky-social/indigo/did"
20 "github.com/bluesky-social/indigo/plc"
21 "github.com/bluesky-social/indigo/util"
22)
23
24type Client struct {
25 plc.CachingDidResolver
26
27 h *http.Client
28
29 service string
30 rotationKey *crypto.PrivateKeyK256
31 recoveryKey string
32 pdsHostname string
33}
34
35type ClientArgs struct {
36 Service string
37 RotationKey []byte
38 RecoveryKey string
39 PdsHostname string
40}
41
42func NewClient(args *ClientArgs) (*Client, error) {
43 if args.Service == "" {
44 args.Service = "https://plc.directory"
45 }
46
47 rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey))
48 if err != nil {
49 return nil, err
50 }
51
52 resolver := did.NewMultiResolver()
53 return &Client{
54 CachingDidResolver: *plc.NewCachingDidResolver(resolver, 5*time.Minute, 100_000),
55 h: util.RobustHTTPClient(),
56 service: args.Service,
57 rotationKey: rk,
58 recoveryKey: args.RecoveryKey,
59 pdsHostname: args.PdsHostname,
60 }, nil
61}
62
63func (c *Client) CreateDID(ctx context.Context, sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, map[string]any, error) {
64 pubrotkey, err := c.rotationKey.PublicKey()
65 if err != nil {
66 return "", nil, err
67 }
68
69 // todo
70 rotationKeys := []string{pubrotkey.DIDKey()}
71 if c.recoveryKey != "" {
72 rotationKeys = []string{c.recoveryKey, rotationKeys[0]}
73 }
74 if recovery != "" {
75 rotationKeys = func(recovery string) []string {
76 newRotationKeys := []string{recovery}
77 for _, k := range rotationKeys {
78 newRotationKeys = append(newRotationKeys, k)
79 }
80 return newRotationKeys
81 }(recovery)
82 }
83
84 op, err := c.FormatAndSignAtprotoOp(sigkey, handle, rotationKeys, nil)
85 if err != nil {
86 return "", nil, err
87 }
88
89 did, err := didForCreateOp(op)
90 if err != nil {
91 return "", nil, err
92 }
93
94 return did, op, nil
95}
96
97func (c *Client) UpdateUserHandle(ctx context.Context, didstr string, nhandle string) error {
98 return nil
99}
100
101func (c *Client) FormatAndSignAtprotoOp(sigkey *crypto.PrivateKeyK256, handle string, rotationKeys []string, prev *string) (map[string]any, error) {
102 pubsigkey, err := sigkey.PublicKey()
103 if err != nil {
104 return nil, err
105 }
106
107 op := map[string]any{
108 "type": "plc_operation",
109 "verificationMethods": map[string]string{
110 "atproto": pubsigkey.DIDKey(),
111 },
112 "rotationKeys": rotationKeys,
113 "alsoKnownAs": []string{"at://" + handle},
114 "services": map[string]any{
115 "atproto_pds": map[string]string{
116 "type": "AtprotoPersonalDataServer",
117 "endpoint": "https://" + c.pdsHostname,
118 },
119 },
120 "prev": prev,
121 }
122
123 b, err := data.MarshalCBOR(op)
124 if err != nil {
125 return nil, err
126 }
127
128 sig, err := c.rotationKey.HashAndSign(b)
129 if err != nil {
130 return nil, err
131 }
132
133 op["sig"] = base64.RawURLEncoding.EncodeToString(sig)
134
135 return op, nil
136}
137
138func didForCreateOp(op map[string]any) (string, error) {
139 b, err := data.MarshalCBOR(op)
140 if err != nil {
141 return "", err
142 }
143
144 h := sha256.New()
145 h.Write(b)
146 bs := h.Sum(nil)
147
148 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(bs))
149
150 return "did:plc:" + b32[0:24], nil
151}
152
153func (c *Client) SendOperation(ctx context.Context, did string, op any) error {
154 b, err := json.Marshal(op)
155 if err != nil {
156 return err
157 }
158
159 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b))
160 if err != nil {
161 return err
162 }
163
164 req.Header.Add("content-type", "application/json")
165
166 resp, err := c.h.Do(req)
167 if err != nil {
168 return err
169 }
170 defer resp.Body.Close()
171
172 fmt.Println(resp.StatusCode)
173
174 b, err = io.ReadAll(resp.Body)
175 if err != nil {
176 return err
177 }
178
179 fmt.Println(string(b))
180
181 return nil
182}