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}