Monorepo for Tangled
tangled.org
1package knotclient
2
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "io"
11 "log"
12 "net/http"
13 "net/url"
14 "strconv"
15 "time"
16
17 "tangled.sh/tangled.sh/core/types"
18)
19
20type SignerTransport struct {
21 Secret string
22}
23
24func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
25 timestamp := time.Now().Format(time.RFC3339)
26 mac := hmac.New(sha256.New, []byte(s.Secret))
27 message := req.Method + req.URL.Path + timestamp
28 mac.Write([]byte(message))
29 signature := hex.EncodeToString(mac.Sum(nil))
30 req.Header.Set("X-Signature", signature)
31 req.Header.Set("X-Timestamp", timestamp)
32 return http.DefaultTransport.RoundTrip(req)
33}
34
35type SignedClient struct {
36 Secret string
37 Url *url.URL
38 client *http.Client
39}
40
41func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
42 client := &http.Client{
43 Timeout: 5 * time.Second,
44 Transport: SignerTransport{
45 Secret: secret,
46 },
47 }
48
49 scheme := "https"
50 if dev {
51 scheme = "http"
52 }
53 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
54 if err != nil {
55 return nil, err
56 }
57
58 signedClient := &SignedClient{
59 Secret: secret,
60 client: client,
61 Url: url,
62 }
63
64 return signedClient, nil
65}
66
67func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
68 return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
69}
70
71func (s *SignedClient) Init(did string) (*http.Response, error) {
72 const (
73 Method = "POST"
74 Endpoint = "/init"
75 )
76
77 body, _ := json.Marshal(map[string]any{
78 "did": did,
79 })
80
81 req, err := s.newRequest(Method, Endpoint, body)
82 if err != nil {
83 return nil, err
84 }
85
86 return s.client.Do(req)
87}
88
89func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
90 const (
91 Method = "PUT"
92 Endpoint = "/repo/new"
93 )
94
95 body, _ := json.Marshal(map[string]any{
96 "did": did,
97 "name": repoName,
98 "default_branch": defaultBranch,
99 })
100
101 req, err := s.newRequest(Method, Endpoint, body)
102 if err != nil {
103 return nil, err
104 }
105
106 return s.client.Do(req)
107}
108
109func (s *SignedClient) RepoLanguages(ownerDid, source, name, branch string) (*types.RepoLanguageResponse, error) {
110 const (
111 Method = "GET"
112 )
113 endpoint := fmt.Sprintf("/repo/languages/%s", url.PathEscape(branch))
114
115 body, _ := json.Marshal(map[string]any{
116 "did": ownerDid,
117 "source": source,
118 "name": name,
119 })
120
121 req, err := s.newRequest(Method, endpoint, body)
122 if err != nil {
123 return nil, err
124 }
125
126 resp, err := s.client.Do(req)
127 if err != nil {
128 return nil, err
129 }
130
131 var languagePercentages types.RepoLanguageResponse
132 if err := json.NewDecoder(resp.Body).Decode(&languagePercentages); err != nil {
133 log.Printf("failed to decode fork status: %s", err)
134 return nil, err
135 }
136
137 return &languagePercentages, nil
138}
139
140func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
141 const (
142 Method = "GET"
143 )
144 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
145
146 body, _ := json.Marshal(map[string]any{
147 "did": ownerDid,
148 "source": source,
149 "name": name,
150 "hiddenref": hiddenRef,
151 })
152
153 req, err := s.newRequest(Method, endpoint, body)
154 if err != nil {
155 return nil, err
156 }
157
158 return s.client.Do(req)
159}
160
161func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) {
162 const (
163 Method = "POST"
164 )
165 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
166
167 body, _ := json.Marshal(map[string]any{
168 "did": ownerDid,
169 "source": source,
170 "name": name,
171 })
172
173 req, err := s.newRequest(Method, endpoint, body)
174 if err != nil {
175 return nil, err
176 }
177
178 return s.client.Do(req)
179}
180
181func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
182 const (
183 Method = "POST"
184 Endpoint = "/repo/fork"
185 )
186
187 body, _ := json.Marshal(map[string]any{
188 "did": ownerDid,
189 "source": source,
190 "name": name,
191 })
192
193 req, err := s.newRequest(Method, Endpoint, body)
194 if err != nil {
195 return nil, err
196 }
197
198 return s.client.Do(req)
199}
200
201func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
202 const (
203 Method = "DELETE"
204 Endpoint = "/repo"
205 )
206
207 body, _ := json.Marshal(map[string]any{
208 "did": did,
209 "name": repoName,
210 })
211
212 req, err := s.newRequest(Method, Endpoint, body)
213 if err != nil {
214 return nil, err
215 }
216
217 return s.client.Do(req)
218}
219
220func (s *SignedClient) AddMember(did string) (*http.Response, error) {
221 const (
222 Method = "PUT"
223 Endpoint = "/member/add"
224 )
225
226 body, _ := json.Marshal(map[string]any{
227 "did": did,
228 })
229
230 req, err := s.newRequest(Method, Endpoint, body)
231 if err != nil {
232 return nil, err
233 }
234
235 return s.client.Do(req)
236}
237
238func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
239 const (
240 Method = "PUT"
241 )
242 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
243
244 body, _ := json.Marshal(map[string]any{
245 "branch": branch,
246 })
247
248 req, err := s.newRequest(Method, endpoint, body)
249 if err != nil {
250 return nil, err
251 }
252
253 return s.client.Do(req)
254}
255
256func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
257 const (
258 Method = "POST"
259 )
260 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
261
262 body, _ := json.Marshal(map[string]any{
263 "did": memberDid,
264 })
265
266 req, err := s.newRequest(Method, endpoint, body)
267 if err != nil {
268 return nil, err
269 }
270
271 return s.client.Do(req)
272}
273
274func (s *SignedClient) Merge(
275 patch []byte,
276 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
277) (*http.Response, error) {
278 const (
279 Method = "POST"
280 )
281 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
282
283 mr := types.MergeRequest{
284 Branch: branch,
285 CommitMessage: commitMessage,
286 CommitBody: commitBody,
287 AuthorName: authorName,
288 AuthorEmail: authorEmail,
289 Patch: string(patch),
290 }
291
292 body, _ := json.Marshal(mr)
293
294 req, err := s.newRequest(Method, endpoint, body)
295 if err != nil {
296 return nil, err
297 }
298
299 return s.client.Do(req)
300}
301
302func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
303 const (
304 Method = "POST"
305 )
306 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
307
308 body, _ := json.Marshal(map[string]any{
309 "patch": string(patch),
310 "branch": branch,
311 })
312
313 req, err := s.newRequest(Method, endpoint, body)
314 if err != nil {
315 return nil, err
316 }
317
318 return s.client.Do(req)
319}
320
321func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
322 const (
323 Method = "POST"
324 )
325 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
326
327 req, err := s.newRequest(Method, endpoint, nil)
328 if err != nil {
329 return nil, err
330 }
331
332 return s.client.Do(req)
333}
334
335type UnsignedClient struct {
336 Url *url.URL
337 client *http.Client
338}
339
340func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
341 client := &http.Client{
342 Timeout: 5 * time.Second,
343 }
344
345 scheme := "https"
346 if dev {
347 scheme = "http"
348 }
349 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
350 if err != nil {
351 return nil, err
352 }
353
354 unsignedClient := &UnsignedClient{
355 client: client,
356 Url: url,
357 }
358
359 return unsignedClient, nil
360}
361
362func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
363 reqUrl := us.Url.JoinPath(endpoint)
364
365 // add query parameters
366 if query != nil {
367 reqUrl.RawQuery = query.Encode()
368 }
369
370 return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
371}
372
373func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
374 const (
375 Method = "GET"
376 )
377
378 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
379 if ref == "" {
380 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
381 }
382
383 req, err := us.newRequest(Method, endpoint, nil, nil)
384 if err != nil {
385 return nil, err
386 }
387
388 return us.client.Do(req)
389}
390
391func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
392 const (
393 Method = "GET"
394 )
395
396 endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
397
398 query := url.Values{}
399 query.Add("page", strconv.Itoa(page))
400 query.Add("per_page", strconv.Itoa(60))
401
402 req, err := us.newRequest(Method, endpoint, query, nil)
403 if err != nil {
404 return nil, err
405 }
406
407 return us.client.Do(req)
408}
409
410func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
411 const (
412 Method = "GET"
413 )
414
415 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
416
417 req, err := us.newRequest(Method, endpoint, nil, nil)
418 if err != nil {
419 return nil, err
420 }
421
422 return us.client.Do(req)
423}
424
425func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
426 const (
427 Method = "GET"
428 )
429
430 endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
431
432 req, err := us.newRequest(Method, endpoint, nil, nil)
433 if err != nil {
434 return nil, err
435 }
436
437 resp, err := us.client.Do(req)
438 if err != nil {
439 return nil, err
440 }
441
442 body, err := io.ReadAll(resp.Body)
443 if err != nil {
444 return nil, err
445 }
446
447 var result types.RepoTagsResponse
448 err = json.Unmarshal(body, &result)
449 if err != nil {
450 return nil, err
451 }
452
453 return &result, nil
454}
455
456func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
457 const (
458 Method = "GET"
459 )
460
461 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
462
463 req, err := us.newRequest(Method, endpoint, nil, nil)
464 if err != nil {
465 return nil, err
466 }
467
468 return us.client.Do(req)
469}
470
471func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
472 const (
473 Method = "GET"
474 )
475
476 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
477
478 req, err := us.newRequest(Method, endpoint, nil, nil)
479 if err != nil {
480 return nil, err
481 }
482
483 resp, err := us.client.Do(req)
484 if err != nil {
485 return nil, err
486 }
487 defer resp.Body.Close()
488
489 var defaultBranch types.RepoDefaultBranchResponse
490 if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
491 return nil, err
492 }
493
494 return &defaultBranch, nil
495}
496
497func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
498 const (
499 Method = "GET"
500 Endpoint = "/capabilities"
501 )
502
503 req, err := us.newRequest(Method, Endpoint, nil, nil)
504 if err != nil {
505 return nil, err
506 }
507
508 resp, err := us.client.Do(req)
509 if err != nil {
510 return nil, err
511 }
512 defer resp.Body.Close()
513
514 var capabilities types.Capabilities
515 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
516 return nil, err
517 }
518
519 return &capabilities, nil
520}
521
522func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
523 const (
524 Method = "GET"
525 )
526
527 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
528
529 req, err := us.newRequest(Method, endpoint, nil, nil)
530 if err != nil {
531 return nil, fmt.Errorf("Failed to create request.")
532 }
533
534 compareResp, err := us.client.Do(req)
535 if err != nil {
536 return nil, fmt.Errorf("Failed to create request.")
537 }
538 defer compareResp.Body.Close()
539
540 switch compareResp.StatusCode {
541 case 404:
542 case 400:
543 return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
544 }
545
546 respBody, err := io.ReadAll(compareResp.Body)
547 if err != nil {
548 log.Println("failed to compare across branches")
549 return nil, fmt.Errorf("Failed to compare branches.")
550 }
551 defer compareResp.Body.Close()
552
553 var formatPatchResponse types.RepoFormatPatchResponse
554 err = json.Unmarshal(respBody, &formatPatchResponse)
555 if err != nil {
556 log.Println("failed to unmarshal format-patch response", err)
557 return nil, fmt.Errorf("failed to compare branches.")
558 }
559
560 return &formatPatchResponse, nil
561}