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, repoName, ref string) (*types.RepoLanguageResponse, error) {
110 const (
111 Method = "GET"
112 )
113 endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref))
114
115 req, err := s.newRequest(Method, endpoint, nil)
116 if err != nil {
117 return nil, err
118 }
119
120 resp, err := s.client.Do(req)
121 if err != nil {
122 return nil, err
123 }
124
125 var result types.RepoLanguageResponse
126 if resp.StatusCode != http.StatusOK {
127 log.Println("failed to calculate languages", resp.Status)
128 return &types.RepoLanguageResponse{}, nil
129 }
130
131 body, err := io.ReadAll(resp.Body)
132 if err != nil {
133 return nil, err
134 }
135
136 err = json.Unmarshal(body, &result)
137 if err != nil {
138 return nil, err
139 }
140
141 return &result, nil
142}
143
144func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
145 const (
146 Method = "GET"
147 )
148 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
149
150 body, _ := json.Marshal(map[string]any{
151 "did": ownerDid,
152 "source": source,
153 "name": name,
154 "hiddenref": hiddenRef,
155 })
156
157 req, err := s.newRequest(Method, endpoint, body)
158 if err != nil {
159 return nil, err
160 }
161
162 return s.client.Do(req)
163}
164
165func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) {
166 const (
167 Method = "POST"
168 )
169 endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
170
171 body, _ := json.Marshal(map[string]any{
172 "did": ownerDid,
173 "source": source,
174 "name": name,
175 })
176
177 req, err := s.newRequest(Method, endpoint, body)
178 if err != nil {
179 return nil, err
180 }
181
182 return s.client.Do(req)
183}
184
185func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
186 const (
187 Method = "POST"
188 Endpoint = "/repo/fork"
189 )
190
191 body, _ := json.Marshal(map[string]any{
192 "did": ownerDid,
193 "source": source,
194 "name": name,
195 })
196
197 req, err := s.newRequest(Method, Endpoint, body)
198 if err != nil {
199 return nil, err
200 }
201
202 return s.client.Do(req)
203}
204
205func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
206 const (
207 Method = "DELETE"
208 Endpoint = "/repo"
209 )
210
211 body, _ := json.Marshal(map[string]any{
212 "did": did,
213 "name": repoName,
214 })
215
216 req, err := s.newRequest(Method, Endpoint, body)
217 if err != nil {
218 return nil, err
219 }
220
221 return s.client.Do(req)
222}
223
224func (s *SignedClient) AddMember(did string) (*http.Response, error) {
225 const (
226 Method = "PUT"
227 Endpoint = "/member/add"
228 )
229
230 body, _ := json.Marshal(map[string]any{
231 "did": did,
232 })
233
234 req, err := s.newRequest(Method, Endpoint, body)
235 if err != nil {
236 return nil, err
237 }
238
239 return s.client.Do(req)
240}
241
242func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
243 const (
244 Method = "PUT"
245 )
246 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
247
248 body, _ := json.Marshal(map[string]any{
249 "branch": branch,
250 })
251
252 req, err := s.newRequest(Method, endpoint, body)
253 if err != nil {
254 return nil, err
255 }
256
257 return s.client.Do(req)
258}
259
260func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
261 const (
262 Method = "POST"
263 )
264 endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
265
266 body, _ := json.Marshal(map[string]any{
267 "did": memberDid,
268 })
269
270 req, err := s.newRequest(Method, endpoint, body)
271 if err != nil {
272 return nil, err
273 }
274
275 return s.client.Do(req)
276}
277
278func (s *SignedClient) Merge(
279 patch []byte,
280 ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
281) (*http.Response, error) {
282 const (
283 Method = "POST"
284 )
285 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
286
287 mr := types.MergeRequest{
288 Branch: branch,
289 CommitMessage: commitMessage,
290 CommitBody: commitBody,
291 AuthorName: authorName,
292 AuthorEmail: authorEmail,
293 Patch: string(patch),
294 }
295
296 body, _ := json.Marshal(mr)
297
298 req, err := s.newRequest(Method, endpoint, body)
299 if err != nil {
300 return nil, err
301 }
302
303 return s.client.Do(req)
304}
305
306func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
307 const (
308 Method = "POST"
309 )
310 endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
311
312 body, _ := json.Marshal(map[string]any{
313 "patch": string(patch),
314 "branch": branch,
315 })
316
317 req, err := s.newRequest(Method, endpoint, body)
318 if err != nil {
319 return nil, err
320 }
321
322 return s.client.Do(req)
323}
324
325func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
326 const (
327 Method = "POST"
328 )
329 endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
330
331 req, err := s.newRequest(Method, endpoint, nil)
332 if err != nil {
333 return nil, err
334 }
335
336 return s.client.Do(req)
337}
338
339type UnsignedClient struct {
340 Url *url.URL
341 client *http.Client
342}
343
344func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
345 client := &http.Client{
346 Timeout: 5 * time.Second,
347 }
348
349 scheme := "https"
350 if dev {
351 scheme = "http"
352 }
353 url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
354 if err != nil {
355 return nil, err
356 }
357
358 unsignedClient := &UnsignedClient{
359 client: client,
360 Url: url,
361 }
362
363 return unsignedClient, nil
364}
365
366func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
367 reqUrl := us.Url.JoinPath(endpoint)
368
369 // add query parameters
370 if query != nil {
371 reqUrl.RawQuery = query.Encode()
372 }
373
374 return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
375}
376
377func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
378 const (
379 Method = "GET"
380 )
381
382 endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
383 if ref == "" {
384 endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
385 }
386
387 req, err := us.newRequest(Method, endpoint, nil, nil)
388 if err != nil {
389 return nil, err
390 }
391
392 return us.client.Do(req)
393}
394
395func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
396 const (
397 Method = "GET"
398 )
399
400 endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
401
402 query := url.Values{}
403 query.Add("page", strconv.Itoa(page))
404 query.Add("per_page", strconv.Itoa(60))
405
406 req, err := us.newRequest(Method, endpoint, query, nil)
407 if err != nil {
408 return nil, err
409 }
410
411 return us.client.Do(req)
412}
413
414func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
415 const (
416 Method = "GET"
417 )
418
419 endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
420
421 req, err := us.newRequest(Method, endpoint, nil, nil)
422 if err != nil {
423 return nil, err
424 }
425
426 return us.client.Do(req)
427}
428
429func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
430 const (
431 Method = "GET"
432 )
433
434 endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
435
436 req, err := us.newRequest(Method, endpoint, nil, nil)
437 if err != nil {
438 return nil, err
439 }
440
441 resp, err := us.client.Do(req)
442 if err != nil {
443 return nil, err
444 }
445
446 body, err := io.ReadAll(resp.Body)
447 if err != nil {
448 return nil, err
449 }
450
451 var result types.RepoTagsResponse
452 err = json.Unmarshal(body, &result)
453 if err != nil {
454 return nil, err
455 }
456
457 return &result, nil
458}
459
460func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
461 const (
462 Method = "GET"
463 )
464
465 endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
466
467 req, err := us.newRequest(Method, endpoint, nil, nil)
468 if err != nil {
469 return nil, err
470 }
471
472 return us.client.Do(req)
473}
474
475func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
476 const (
477 Method = "GET"
478 )
479
480 endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
481
482 req, err := us.newRequest(Method, endpoint, nil, nil)
483 if err != nil {
484 return nil, err
485 }
486
487 resp, err := us.client.Do(req)
488 if err != nil {
489 return nil, err
490 }
491 defer resp.Body.Close()
492
493 var defaultBranch types.RepoDefaultBranchResponse
494 if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
495 return nil, err
496 }
497
498 return &defaultBranch, nil
499}
500
501func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
502 const (
503 Method = "GET"
504 Endpoint = "/capabilities"
505 )
506
507 req, err := us.newRequest(Method, Endpoint, nil, nil)
508 if err != nil {
509 return nil, err
510 }
511
512 resp, err := us.client.Do(req)
513 if err != nil {
514 return nil, err
515 }
516 defer resp.Body.Close()
517
518 var capabilities types.Capabilities
519 if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
520 return nil, err
521 }
522
523 return &capabilities, nil
524}
525
526func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
527 const (
528 Method = "GET"
529 )
530
531 endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
532
533 req, err := us.newRequest(Method, endpoint, nil, nil)
534 if err != nil {
535 return nil, fmt.Errorf("Failed to create request.")
536 }
537
538 compareResp, err := us.client.Do(req)
539 if err != nil {
540 return nil, fmt.Errorf("Failed to create request.")
541 }
542 defer compareResp.Body.Close()
543
544 switch compareResp.StatusCode {
545 case 404:
546 case 400:
547 return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
548 }
549
550 respBody, err := io.ReadAll(compareResp.Body)
551 if err != nil {
552 log.Println("failed to compare across branches")
553 return nil, fmt.Errorf("Failed to compare branches.")
554 }
555 defer compareResp.Body.Close()
556
557 var formatPatchResponse types.RepoFormatPatchResponse
558 err = json.Unmarshal(respBody, &formatPatchResponse)
559 if err != nil {
560 log.Println("failed to unmarshal format-patch response", err)
561 return nil, fmt.Errorf("failed to compare branches.")
562 }
563
564 return &formatPatchResponse, nil
565}