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