this repo has no description
1package state 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "math/rand/v2" 10 "net/http" 11 "path" 12 "slices" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/bluesky-social/indigo/atproto/identity" 18 "github.com/bluesky-social/indigo/atproto/syntax" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 "github.com/sotangled/tangled/api/tangled" 22 "github.com/sotangled/tangled/appview/auth" 23 "github.com/sotangled/tangled/appview/db" 24 "github.com/sotangled/tangled/appview/pages" 25 "github.com/sotangled/tangled/types" 26 27 comatproto "github.com/bluesky-social/indigo/api/atproto" 28 lexutil "github.com/bluesky-social/indigo/lex/util" 29) 30 31func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 32 ref := chi.URLParam(r, "ref") 33 f, err := fullyResolvedRepo(r) 34 if err != nil { 35 log.Println("failed to fully resolve repo", err) 36 return 37 } 38 39 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 40 if err != nil { 41 log.Printf("failed to create unsigned client for %s", f.Knot) 42 s.pages.Error503(w) 43 return 44 } 45 46 resp, err := us.Index(f.OwnerDid(), f.RepoName, ref) 47 if err != nil { 48 s.pages.Error503(w) 49 log.Println("failed to reach knotserver", err) 50 return 51 } 52 defer resp.Body.Close() 53 54 body, err := io.ReadAll(resp.Body) 55 if err != nil { 56 log.Printf("Error reading response body: %v", err) 57 return 58 } 59 60 var result types.RepoIndexResponse 61 err = json.Unmarshal(body, &result) 62 if err != nil { 63 log.Printf("Error unmarshalling response body: %v", err) 64 return 65 } 66 67 tagMap := make(map[string][]string) 68 for _, tag := range result.Tags { 69 hash := tag.Hash 70 tagMap[hash] = append(tagMap[hash], tag.Name) 71 } 72 73 for _, branch := range result.Branches { 74 hash := branch.Hash 75 tagMap[hash] = append(tagMap[hash], branch.Name) 76 } 77 78 user := s.auth.GetUser(r) 79 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 80 LoggedInUser: user, 81 RepoInfo: f.RepoInfo(s, user), 82 TagMap: tagMap, 83 RepoIndexResponse: result, 84 }) 85 86 return 87} 88 89func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 90 f, err := fullyResolvedRepo(r) 91 if err != nil { 92 log.Println("failed to fully resolve repo", err) 93 return 94 } 95 96 page := 1 97 if r.URL.Query().Get("page") != "" { 98 page, err = strconv.Atoi(r.URL.Query().Get("page")) 99 if err != nil { 100 page = 1 101 } 102 } 103 104 ref := chi.URLParam(r, "ref") 105 106 protocol := "http" 107 if !s.config.Dev { 108 protocol = "https" 109 } 110 111 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=30", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page)) 112 if err != nil { 113 log.Println("failed to reach knotserver", err) 114 return 115 } 116 117 body, err := io.ReadAll(resp.Body) 118 if err != nil { 119 log.Printf("error reading response body: %v", err) 120 return 121 } 122 123 var repolog types.RepoLogResponse 124 err = json.Unmarshal(body, &repolog) 125 if err != nil { 126 log.Println("failed to parse json response", err) 127 return 128 } 129 130 user := s.auth.GetUser(r) 131 s.pages.RepoLog(w, pages.RepoLogParams{ 132 LoggedInUser: user, 133 RepoInfo: f.RepoInfo(s, user), 134 RepoLogResponse: repolog, 135 }) 136 return 137} 138 139func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 140 f, err := fullyResolvedRepo(r) 141 if err != nil { 142 log.Println("failed to get repo and knot", err) 143 w.WriteHeader(http.StatusBadRequest) 144 return 145 } 146 147 user := s.auth.GetUser(r) 148 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 149 RepoInfo: f.RepoInfo(s, user), 150 }) 151 return 152} 153 154func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 155 f, err := fullyResolvedRepo(r) 156 if err != nil { 157 log.Println("failed to get repo and knot", err) 158 w.WriteHeader(http.StatusBadRequest) 159 return 160 } 161 162 repoAt := f.RepoAt 163 rkey := repoAt.RecordKey().String() 164 if rkey == "" { 165 log.Println("invalid aturi for repo", err) 166 w.WriteHeader(http.StatusInternalServerError) 167 return 168 } 169 170 user := s.auth.GetUser(r) 171 172 switch r.Method { 173 case http.MethodGet: 174 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 175 RepoInfo: f.RepoInfo(s, user), 176 }) 177 return 178 case http.MethodPut: 179 user := s.auth.GetUser(r) 180 newDescription := r.FormValue("description") 181 client, _ := s.auth.AuthorizedClient(r) 182 183 // optimistic update 184 err = db.UpdateDescription(s.db, string(repoAt), newDescription) 185 if err != nil { 186 log.Println("failed to perferom update-description query", err) 187 s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 188 return 189 } 190 191 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 192 // 193 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 194 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) 195 if err != nil { 196 // failed to get record 197 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 198 return 199 } 200 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 201 Collection: tangled.RepoNSID, 202 Repo: user.Did, 203 Rkey: rkey, 204 SwapRecord: ex.Cid, 205 Record: &lexutil.LexiconTypeDecoder{ 206 Val: &tangled.Repo{ 207 Knot: f.Knot, 208 Name: f.RepoName, 209 Owner: user.Did, 210 AddedAt: &f.AddedAt, 211 Description: &newDescription, 212 }, 213 }, 214 }) 215 216 if err != nil { 217 log.Println("failed to perferom update-description query", err) 218 // failed to get record 219 s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 220 return 221 } 222 223 newRepoInfo := f.RepoInfo(s, user) 224 newRepoInfo.Description = newDescription 225 226 s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 227 RepoInfo: newRepoInfo, 228 }) 229 return 230 } 231} 232 233func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) { 234 user := s.auth.GetUser(r) 235 f, err := fullyResolvedRepo(r) 236 if err != nil { 237 log.Println("failed to get repo and knot", err) 238 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 239 return 240 } 241 242 prId := chi.URLParam(r, "pull") 243 prIdInt, err := strconv.Atoi(prId) 244 if err != nil { 245 http.Error(w, "bad pr id", http.StatusBadRequest) 246 log.Println("failed to parse pr id", err) 247 return 248 } 249 250 patch := r.FormValue("patch") 251 if patch == "" { 252 s.pages.Notice(w, "pull-error", "Patch is required.") 253 return 254 } 255 256 // Get pull information before updating to get the atproto record URI 257 pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt) 258 if err != nil { 259 log.Println("failed to get pull information", err) 260 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 261 return 262 } 263 264 // Start a transaction for database operations 265 tx, err := s.db.BeginTx(r.Context(), nil) 266 if err != nil { 267 log.Println("failed to start transaction", err) 268 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 269 return 270 } 271 272 // Set up deferred rollback that will be overridden by commit if successful 273 defer tx.Rollback() 274 275 // Update patch in the database within transaction 276 err = db.EditPatch(tx, f.RepoAt, prIdInt, patch) 277 if err != nil { 278 log.Println("failed to update patch", err) 279 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 280 return 281 } 282 283 // Update the atproto record 284 client, _ := s.auth.AuthorizedClient(r) 285 pullAt := pull.PullAt 286 287 // Get the existing record first 288 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pullAt.RecordKey().String()) 289 if err != nil { 290 log.Println("failed to get existing pull record", err) 291 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 292 return 293 } 294 295 // Update the record 296 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 297 Collection: tangled.RepoPullNSID, 298 Repo: user.Did, 299 Rkey: pullAt.RecordKey().String(), 300 SwapRecord: ex.Cid, 301 Record: &lexutil.LexiconTypeDecoder{ 302 Val: &tangled.RepoPull{ 303 Title: pull.Title, 304 PullId: int64(pull.PullId), 305 TargetRepo: string(f.RepoAt), 306 TargetBranch: pull.TargetBranch, 307 Patch: patch, 308 }, 309 }, 310 }) 311 312 if err != nil { 313 log.Println("failed to update pull record in atproto", err) 314 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 315 return 316 } 317 318 // Commit the transaction now that both operations have succeeded 319 err = tx.Commit() 320 if err != nil { 321 log.Println("failed to commit transaction", err) 322 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 323 return 324 } 325 326 targetBranch := pull.TargetBranch 327 328 // Perform merge check 329 secret, err := db.GetRegistrationKey(s.db, f.Knot) 330 if err != nil { 331 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 332 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 333 return 334 } 335 336 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 337 if err != nil { 338 log.Printf("failed to create signed client for %s", f.Knot) 339 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 340 return 341 } 342 343 resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch) 344 if err != nil { 345 log.Println("failed to check mergeability", err) 346 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 347 return 348 } 349 350 respBody, err := io.ReadAll(resp.Body) 351 if err != nil { 352 log.Println("failed to read knotserver response body") 353 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 354 return 355 } 356 357 var mergeCheckResponse types.MergeCheckResponse 358 err = json.Unmarshal(respBody, &mergeCheckResponse) 359 if err != nil { 360 log.Println("failed to unmarshal merge check response", err) 361 s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 362 return 363 } 364 365 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt)) 366 return 367} 368 369func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 370 user := s.auth.GetUser(r) 371 f, err := fullyResolvedRepo(r) 372 if err != nil { 373 log.Println("failed to get repo and knot", err) 374 return 375 } 376 377 switch r.Method { 378 case http.MethodGet: 379 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 380 if err != nil { 381 log.Printf("failed to create unsigned client for %s", f.Knot) 382 s.pages.Error503(w) 383 return 384 } 385 386 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 387 if err != nil { 388 log.Println("failed to reach knotserver", err) 389 return 390 } 391 392 body, err := io.ReadAll(resp.Body) 393 if err != nil { 394 log.Printf("Error reading response body: %v", err) 395 return 396 } 397 398 var result types.RepoBranchesResponse 399 err = json.Unmarshal(body, &result) 400 if err != nil { 401 log.Println("failed to parse response:", err) 402 return 403 } 404 405 s.pages.RepoNewPull(w, pages.RepoNewPullParams{ 406 LoggedInUser: user, 407 RepoInfo: f.RepoInfo(s, user), 408 Branches: result.Branches, 409 }) 410 case http.MethodPost: 411 title := r.FormValue("title") 412 body := r.FormValue("body") 413 targetBranch := r.FormValue("targetBranch") 414 patch := r.FormValue("patch") 415 416 if title == "" || body == "" || patch == "" || targetBranch == "" { 417 s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 418 return 419 } 420 421 tx, err := s.db.BeginTx(r.Context(), nil) 422 if err != nil { 423 log.Println("failed to start tx") 424 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 425 return 426 } 427 428 defer func() { 429 tx.Rollback() 430 err = s.enforcer.E.LoadPolicy() 431 if err != nil { 432 log.Println("failed to rollback policies") 433 } 434 }() 435 436 err = db.NewPull(tx, &db.Pull{ 437 Title: title, 438 Body: body, 439 TargetBranch: targetBranch, 440 Patch: patch, 441 OwnerDid: user.Did, 442 RepoAt: f.RepoAt, 443 }) 444 if err != nil { 445 log.Println("failed to create pull request", err) 446 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 447 return 448 } 449 client, _ := s.auth.AuthorizedClient(r) 450 pullId, err := db.NextPullId(s.db, f.RepoAt) 451 if err != nil { 452 log.Println("failed to get pull id", err) 453 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 454 return 455 } 456 457 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 458 Collection: tangled.RepoPullNSID, 459 Repo: user.Did, 460 Rkey: s.TID(), 461 Record: &lexutil.LexiconTypeDecoder{ 462 Val: &tangled.RepoPull{ 463 Title: title, 464 PullId: int64(pullId), 465 TargetRepo: string(f.RepoAt), 466 TargetBranch: targetBranch, 467 Patch: patch, 468 }, 469 }, 470 }) 471 472 err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 473 if err != nil { 474 log.Println("failed to get pull id", err) 475 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 476 return 477 } 478 479 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 480 return 481 } 482} 483 484func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 485 user := s.auth.GetUser(r) 486 f, err := fullyResolvedRepo(r) 487 if err != nil { 488 log.Println("failed to get repo and knot", err) 489 return 490 } 491 492 prId := chi.URLParam(r, "pull") 493 prIdInt, err := strconv.Atoi(prId) 494 if err != nil { 495 http.Error(w, "bad pr id", http.StatusBadRequest) 496 log.Println("failed to parse pr id", err) 497 return 498 } 499 500 pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt) 501 if err != nil { 502 log.Println("failed to get pr and comments", err) 503 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 504 return 505 } 506 507 pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid) 508 if err != nil { 509 log.Println("failed to resolve pull owner", err) 510 } 511 512 identsToResolve := make([]string, len(comments)) 513 for i, comment := range comments { 514 identsToResolve[i] = comment.OwnerDid 515 } 516 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 517 didHandleMap := make(map[string]string) 518 for _, identity := range resolvedIds { 519 if !identity.Handle.IsInvalidHandle() { 520 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 521 } else { 522 didHandleMap[identity.DID.String()] = identity.DID.String() 523 } 524 } 525 526 secret, err := db.GetRegistrationKey(s.db, f.Knot) 527 if err != nil { 528 log.Printf("failed to get registration key for %s", f.Knot) 529 s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 530 return 531 } 532 533 var mergeCheckResponse types.MergeCheckResponse 534 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 535 if err == nil { 536 resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch) 537 if err != nil { 538 log.Println("failed to check for mergeability:", err) 539 } else { 540 respBody, err := io.ReadAll(resp.Body) 541 if err != nil { 542 log.Println("failed to read merge check response body") 543 } else { 544 err = json.Unmarshal(respBody, &mergeCheckResponse) 545 if err != nil { 546 log.Println("failed to unmarshal merge check response", err) 547 } 548 } 549 } 550 } else { 551 log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 552 } 553 554 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 555 LoggedInUser: user, 556 RepoInfo: f.RepoInfo(s, user), 557 Pull: *pr, 558 Comments: comments, 559 PullOwnerHandle: pullOwnerIdent.Handle.String(), 560 DidHandleMap: didHandleMap, 561 MergeCheck: mergeCheckResponse, 562 }) 563} 564 565func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 566 f, err := fullyResolvedRepo(r) 567 if err != nil { 568 log.Println("failed to fully resolve repo", err) 569 return 570 } 571 ref := chi.URLParam(r, "ref") 572 protocol := "http" 573 if !s.config.Dev { 574 protocol = "https" 575 } 576 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 577 if err != nil { 578 log.Println("failed to reach knotserver", err) 579 return 580 } 581 582 body, err := io.ReadAll(resp.Body) 583 if err != nil { 584 log.Printf("Error reading response body: %v", err) 585 return 586 } 587 588 var result types.RepoCommitResponse 589 err = json.Unmarshal(body, &result) 590 if err != nil { 591 log.Println("failed to parse response:", err) 592 return 593 } 594 595 user := s.auth.GetUser(r) 596 s.pages.RepoCommit(w, pages.RepoCommitParams{ 597 LoggedInUser: user, 598 RepoInfo: f.RepoInfo(s, user), 599 RepoCommitResponse: result, 600 }) 601 return 602} 603 604func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 605 f, err := fullyResolvedRepo(r) 606 if err != nil { 607 log.Println("failed to fully resolve repo", err) 608 return 609 } 610 611 ref := chi.URLParam(r, "ref") 612 treePath := chi.URLParam(r, "*") 613 protocol := "http" 614 if !s.config.Dev { 615 protocol = "https" 616 } 617 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 618 if err != nil { 619 log.Println("failed to reach knotserver", err) 620 return 621 } 622 623 body, err := io.ReadAll(resp.Body) 624 if err != nil { 625 log.Printf("Error reading response body: %v", err) 626 return 627 } 628 629 var result types.RepoTreeResponse 630 err = json.Unmarshal(body, &result) 631 if err != nil { 632 log.Println("failed to parse response:", err) 633 return 634 } 635 636 user := s.auth.GetUser(r) 637 638 var breadcrumbs [][]string 639 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 640 if treePath != "" { 641 for idx, elem := range strings.Split(treePath, "/") { 642 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 643 } 644 } 645 646 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath) 647 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath) 648 649 s.pages.RepoTree(w, pages.RepoTreeParams{ 650 LoggedInUser: user, 651 BreadCrumbs: breadcrumbs, 652 BaseTreeLink: baseTreeLink, 653 BaseBlobLink: baseBlobLink, 654 RepoInfo: f.RepoInfo(s, user), 655 RepoTreeResponse: result, 656 }) 657 return 658} 659 660func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 661 f, err := fullyResolvedRepo(r) 662 if err != nil { 663 log.Println("failed to get repo and knot", err) 664 return 665 } 666 667 protocol := "http" 668 if !s.config.Dev { 669 protocol = "https" 670 } 671 672 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tags", protocol, f.Knot, f.OwnerDid(), f.RepoName)) 673 if err != nil { 674 log.Println("failed to reach knotserver", err) 675 return 676 } 677 678 body, err := io.ReadAll(resp.Body) 679 if err != nil { 680 log.Printf("Error reading response body: %v", err) 681 return 682 } 683 684 var result types.RepoTagsResponse 685 err = json.Unmarshal(body, &result) 686 if err != nil { 687 log.Println("failed to parse response:", err) 688 return 689 } 690 691 user := s.auth.GetUser(r) 692 s.pages.RepoTags(w, pages.RepoTagsParams{ 693 LoggedInUser: user, 694 RepoInfo: f.RepoInfo(s, user), 695 RepoTagsResponse: result, 696 }) 697 return 698} 699 700func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 701 f, err := fullyResolvedRepo(r) 702 if err != nil { 703 log.Println("failed to get repo and knot", err) 704 return 705 } 706 707 us, err := NewUnsignedClient(f.Knot, s.config.Dev) 708 if err != nil { 709 log.Println("failed to create unsigned client", err) 710 return 711 } 712 713 resp, err := us.Branches(f.OwnerDid(), f.RepoName) 714 if err != nil { 715 log.Println("failed to reach knotserver", err) 716 return 717 } 718 719 body, err := io.ReadAll(resp.Body) 720 if err != nil { 721 log.Printf("Error reading response body: %v", err) 722 return 723 } 724 725 var result types.RepoBranchesResponse 726 err = json.Unmarshal(body, &result) 727 if err != nil { 728 log.Println("failed to parse response:", err) 729 return 730 } 731 732 user := s.auth.GetUser(r) 733 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 734 LoggedInUser: user, 735 RepoInfo: f.RepoInfo(s, user), 736 RepoBranchesResponse: result, 737 }) 738 return 739} 740 741func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 742 f, err := fullyResolvedRepo(r) 743 if err != nil { 744 log.Println("failed to get repo and knot", err) 745 return 746 } 747 748 ref := chi.URLParam(r, "ref") 749 filePath := chi.URLParam(r, "*") 750 protocol := "http" 751 if !s.config.Dev { 752 protocol = "https" 753 } 754 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 755 if err != nil { 756 log.Println("failed to reach knotserver", err) 757 return 758 } 759 760 body, err := io.ReadAll(resp.Body) 761 if err != nil { 762 log.Printf("Error reading response body: %v", err) 763 return 764 } 765 766 var result types.RepoBlobResponse 767 err = json.Unmarshal(body, &result) 768 if err != nil { 769 log.Println("failed to parse response:", err) 770 return 771 } 772 773 var breadcrumbs [][]string 774 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 775 if filePath != "" { 776 for idx, elem := range strings.Split(filePath, "/") { 777 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 778 } 779 } 780 781 user := s.auth.GetUser(r) 782 s.pages.RepoBlob(w, pages.RepoBlobParams{ 783 LoggedInUser: user, 784 RepoInfo: f.RepoInfo(s, user), 785 RepoBlobResponse: result, 786 BreadCrumbs: breadcrumbs, 787 }) 788 return 789} 790 791func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 792 f, err := fullyResolvedRepo(r) 793 if err != nil { 794 log.Println("failed to get repo and knot", err) 795 return 796 } 797 798 collaborator := r.FormValue("collaborator") 799 if collaborator == "" { 800 http.Error(w, "malformed form", http.StatusBadRequest) 801 return 802 } 803 804 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator) 805 if err != nil { 806 w.Write([]byte("failed to resolve collaborator did to a handle")) 807 return 808 } 809 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 810 811 // TODO: create an atproto record for this 812 813 secret, err := db.GetRegistrationKey(s.db, f.Knot) 814 if err != nil { 815 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 816 return 817 } 818 819 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 820 if err != nil { 821 log.Println("failed to create client to ", f.Knot) 822 return 823 } 824 825 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 826 if err != nil { 827 log.Printf("failed to make request to %s: %s", f.Knot, err) 828 return 829 } 830 831 if ksResp.StatusCode != http.StatusNoContent { 832 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 833 return 834 } 835 836 tx, err := s.db.BeginTx(r.Context(), nil) 837 if err != nil { 838 log.Println("failed to start tx") 839 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 840 return 841 } 842 defer func() { 843 tx.Rollback() 844 err = s.enforcer.E.LoadPolicy() 845 if err != nil { 846 log.Println("failed to rollback policies") 847 } 848 }() 849 850 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo()) 851 if err != nil { 852 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 853 return 854 } 855 856 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 857 if err != nil { 858 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 859 return 860 } 861 862 err = tx.Commit() 863 if err != nil { 864 log.Println("failed to commit changes", err) 865 http.Error(w, err.Error(), http.StatusInternalServerError) 866 return 867 } 868 869 err = s.enforcer.E.SavePolicy() 870 if err != nil { 871 log.Println("failed to update ACLs", err) 872 http.Error(w, err.Error(), http.StatusInternalServerError) 873 return 874 } 875 876 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 877 878} 879 880func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 881 f, err := fullyResolvedRepo(r) 882 if err != nil { 883 log.Println("failed to get repo and knot", err) 884 return 885 } 886 887 switch r.Method { 888 case http.MethodGet: 889 // for now, this is just pubkeys 890 user := s.auth.GetUser(r) 891 repoCollaborators, err := f.Collaborators(r.Context(), s) 892 if err != nil { 893 log.Println("failed to get collaborators", err) 894 } 895 896 isCollaboratorInviteAllowed := false 897 if user != nil { 898 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo()) 899 if err == nil && ok { 900 isCollaboratorInviteAllowed = true 901 } 902 } 903 904 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 905 LoggedInUser: user, 906 RepoInfo: f.RepoInfo(s, user), 907 Collaborators: repoCollaborators, 908 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 909 }) 910 } 911} 912 913type FullyResolvedRepo struct { 914 Knot string 915 OwnerId identity.Identity 916 RepoName string 917 RepoAt syntax.ATURI 918 Description string 919 AddedAt string 920} 921 922func (f *FullyResolvedRepo) OwnerDid() string { 923 return f.OwnerId.DID.String() 924} 925 926func (f *FullyResolvedRepo) OwnerHandle() string { 927 return f.OwnerId.Handle.String() 928} 929 930func (f *FullyResolvedRepo) OwnerSlashRepo() string { 931 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 932 return p 933} 934 935func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 936 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) 937 if err != nil { 938 return nil, err 939 } 940 941 var collaborators []pages.Collaborator 942 for _, item := range repoCollaborators { 943 // currently only two roles: owner and member 944 var role string 945 if item[3] == "repo:owner" { 946 role = "owner" 947 } else if item[3] == "repo:collaborator" { 948 role = "collaborator" 949 } else { 950 continue 951 } 952 953 did := item[0] 954 955 c := pages.Collaborator{ 956 Did: did, 957 Handle: "", 958 Role: role, 959 } 960 collaborators = append(collaborators, c) 961 } 962 963 // populate all collborators with handles 964 identsToResolve := make([]string, len(collaborators)) 965 for i, collab := range collaborators { 966 identsToResolve[i] = collab.Did 967 } 968 969 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 970 for i, resolved := range resolvedIdents { 971 if resolved != nil { 972 collaborators[i].Handle = resolved.Handle.String() 973 } 974 } 975 976 return collaborators, nil 977} 978 979func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 980 isStarred := false 981 if u != nil { 982 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 983 } 984 985 starCount, err := db.GetStarCount(s.db, f.RepoAt) 986 if err != nil { 987 log.Println("failed to get star count for ", f.RepoAt) 988 } 989 issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 990 if err != nil { 991 log.Println("failed to get issue count for ", f.RepoAt) 992 } 993 pullCount, err := db.GetPullCount(s.db, f.RepoAt) 994 if err != nil { 995 log.Println("failed to get issue count for ", f.RepoAt) 996 } 997 998 knot := f.Knot 999 if knot == "knot1.tangled.sh" { 1000 knot = "tangled.sh" 1001 } 1002 1003 return pages.RepoInfo{ 1004 OwnerDid: f.OwnerDid(), 1005 OwnerHandle: f.OwnerHandle(), 1006 Name: f.RepoName, 1007 RepoAt: f.RepoAt, 1008 Description: f.Description, 1009 IsStarred: isStarred, 1010 Knot: knot, 1011 Roles: rolesInRepo(s, u, f), 1012 Stats: db.RepoStats{ 1013 StarCount: starCount, 1014 IssueCount: issueCount, 1015 PullCount: pullCount, 1016 }, 1017 } 1018} 1019 1020func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1021 user := s.auth.GetUser(r) 1022 f, err := fullyResolvedRepo(r) 1023 if err != nil { 1024 log.Println("failed to get repo and knot", err) 1025 return 1026 } 1027 1028 issueId := chi.URLParam(r, "issue") 1029 issueIdInt, err := strconv.Atoi(issueId) 1030 if err != nil { 1031 http.Error(w, "bad issue id", http.StatusBadRequest) 1032 log.Println("failed to parse issue id", err) 1033 return 1034 } 1035 1036 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 1037 if err != nil { 1038 log.Println("failed to get issue and comments", err) 1039 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1040 return 1041 } 1042 1043 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 1044 if err != nil { 1045 log.Println("failed to resolve issue owner", err) 1046 } 1047 1048 identsToResolve := make([]string, len(comments)) 1049 for i, comment := range comments { 1050 identsToResolve[i] = comment.OwnerDid 1051 } 1052 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1053 didHandleMap := make(map[string]string) 1054 for _, identity := range resolvedIds { 1055 if !identity.Handle.IsInvalidHandle() { 1056 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1057 } else { 1058 didHandleMap[identity.DID.String()] = identity.DID.String() 1059 } 1060 } 1061 1062 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1063 LoggedInUser: user, 1064 RepoInfo: f.RepoInfo(s, user), 1065 Issue: *issue, 1066 Comments: comments, 1067 1068 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 1069 DidHandleMap: didHandleMap, 1070 }) 1071 1072} 1073 1074func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1075 user := s.auth.GetUser(r) 1076 f, err := fullyResolvedRepo(r) 1077 if err != nil { 1078 log.Println("failed to get repo and knot", err) 1079 return 1080 } 1081 1082 issueId := chi.URLParam(r, "issue") 1083 issueIdInt, err := strconv.Atoi(issueId) 1084 if err != nil { 1085 http.Error(w, "bad issue id", http.StatusBadRequest) 1086 log.Println("failed to parse issue id", err) 1087 return 1088 } 1089 1090 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1091 if err != nil { 1092 log.Println("failed to get issue", err) 1093 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1094 return 1095 } 1096 1097 collaborators, err := f.Collaborators(r.Context(), s) 1098 if err != nil { 1099 log.Println("failed to fetch repo collaborators: %w", err) 1100 } 1101 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1102 return user.Did == collab.Did 1103 }) 1104 isIssueOwner := user.Did == issue.OwnerDid 1105 1106 // TODO: make this more granular 1107 if isIssueOwner || isCollaborator { 1108 1109 closed := tangled.RepoIssueStateClosed 1110 1111 client, _ := s.auth.AuthorizedClient(r) 1112 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1113 Collection: tangled.RepoIssueStateNSID, 1114 Repo: user.Did, 1115 Rkey: s.TID(), 1116 Record: &lexutil.LexiconTypeDecoder{ 1117 Val: &tangled.RepoIssueState{ 1118 Issue: issue.IssueAt, 1119 State: &closed, 1120 }, 1121 }, 1122 }) 1123 1124 if err != nil { 1125 log.Println("failed to update issue state", err) 1126 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1127 return 1128 } 1129 1130 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1131 if err != nil { 1132 log.Println("failed to close issue", err) 1133 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1134 return 1135 } 1136 1137 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1138 return 1139 } else { 1140 log.Println("user is not permitted to close issue") 1141 http.Error(w, "for biden", http.StatusUnauthorized) 1142 return 1143 } 1144} 1145 1146func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1147 user := s.auth.GetUser(r) 1148 f, err := fullyResolvedRepo(r) 1149 if err != nil { 1150 log.Println("failed to get repo and knot", err) 1151 return 1152 } 1153 1154 issueId := chi.URLParam(r, "issue") 1155 issueIdInt, err := strconv.Atoi(issueId) 1156 if err != nil { 1157 http.Error(w, "bad issue id", http.StatusBadRequest) 1158 log.Println("failed to parse issue id", err) 1159 return 1160 } 1161 1162 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1163 if err != nil { 1164 log.Println("failed to get issue", err) 1165 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1166 return 1167 } 1168 1169 collaborators, err := f.Collaborators(r.Context(), s) 1170 if err != nil { 1171 log.Println("failed to fetch repo collaborators: %w", err) 1172 } 1173 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 1174 return user.Did == collab.Did 1175 }) 1176 isIssueOwner := user.Did == issue.OwnerDid 1177 1178 if isCollaborator || isIssueOwner { 1179 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1180 if err != nil { 1181 log.Println("failed to reopen issue", err) 1182 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1183 return 1184 } 1185 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1186 return 1187 } else { 1188 log.Println("user is not the owner of the repo") 1189 http.Error(w, "forbidden", http.StatusUnauthorized) 1190 return 1191 } 1192} 1193 1194func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1195 user := s.auth.GetUser(r) 1196 f, err := fullyResolvedRepo(r) 1197 if err != nil { 1198 log.Println("failed to get repo and knot", err) 1199 return 1200 } 1201 1202 issueId := chi.URLParam(r, "issue") 1203 issueIdInt, err := strconv.Atoi(issueId) 1204 if err != nil { 1205 http.Error(w, "bad issue id", http.StatusBadRequest) 1206 log.Println("failed to parse issue id", err) 1207 return 1208 } 1209 1210 switch r.Method { 1211 case http.MethodPost: 1212 body := r.FormValue("body") 1213 if body == "" { 1214 s.pages.Notice(w, "issue", "Body is required") 1215 return 1216 } 1217 1218 commentId := rand.IntN(1000000) 1219 1220 err := db.NewComment(s.db, &db.Comment{ 1221 OwnerDid: user.Did, 1222 RepoAt: f.RepoAt, 1223 Issue: issueIdInt, 1224 CommentId: commentId, 1225 Body: body, 1226 }) 1227 if err != nil { 1228 log.Println("failed to create comment", err) 1229 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1230 return 1231 } 1232 1233 createdAt := time.Now().Format(time.RFC3339) 1234 commentIdInt64 := int64(commentId) 1235 ownerDid := user.Did 1236 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1237 if err != nil { 1238 log.Println("failed to get issue at", err) 1239 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1240 return 1241 } 1242 1243 atUri := f.RepoAt.String() 1244 client, _ := s.auth.AuthorizedClient(r) 1245 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1246 Collection: tangled.RepoIssueCommentNSID, 1247 Repo: user.Did, 1248 Rkey: s.TID(), 1249 Record: &lexutil.LexiconTypeDecoder{ 1250 Val: &tangled.RepoIssueComment{ 1251 Repo: &atUri, 1252 Issue: issueAt, 1253 CommentId: &commentIdInt64, 1254 Owner: &ownerDid, 1255 Body: &body, 1256 CreatedAt: &createdAt, 1257 }, 1258 }, 1259 }) 1260 if err != nil { 1261 log.Println("failed to create comment", err) 1262 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1263 return 1264 } 1265 1266 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1267 return 1268 } 1269} 1270 1271func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1272 params := r.URL.Query() 1273 state := params.Get("state") 1274 isOpen := true 1275 switch state { 1276 case "open": 1277 isOpen = true 1278 case "closed": 1279 isOpen = false 1280 default: 1281 isOpen = true 1282 } 1283 1284 user := s.auth.GetUser(r) 1285 f, err := fullyResolvedRepo(r) 1286 if err != nil { 1287 log.Println("failed to get repo and knot", err) 1288 return 1289 } 1290 1291 issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1292 if err != nil { 1293 log.Println("failed to get issues", err) 1294 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1295 return 1296 } 1297 1298 identsToResolve := make([]string, len(issues)) 1299 for i, issue := range issues { 1300 identsToResolve[i] = issue.OwnerDid 1301 } 1302 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1303 didHandleMap := make(map[string]string) 1304 for _, identity := range resolvedIds { 1305 if !identity.Handle.IsInvalidHandle() { 1306 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1307 } else { 1308 didHandleMap[identity.DID.String()] = identity.DID.String() 1309 } 1310 } 1311 1312 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1313 LoggedInUser: s.auth.GetUser(r), 1314 RepoInfo: f.RepoInfo(s, user), 1315 Issues: issues, 1316 DidHandleMap: didHandleMap, 1317 FilteringByOpen: isOpen, 1318 }) 1319 return 1320} 1321 1322func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1323 user := s.auth.GetUser(r) 1324 1325 f, err := fullyResolvedRepo(r) 1326 if err != nil { 1327 log.Println("failed to get repo and knot", err) 1328 return 1329 } 1330 1331 switch r.Method { 1332 case http.MethodGet: 1333 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1334 LoggedInUser: user, 1335 RepoInfo: f.RepoInfo(s, user), 1336 }) 1337 case http.MethodPost: 1338 title := r.FormValue("title") 1339 body := r.FormValue("body") 1340 1341 if title == "" || body == "" { 1342 s.pages.Notice(w, "issues", "Title and body are required") 1343 return 1344 } 1345 1346 tx, err := s.db.BeginTx(r.Context(), nil) 1347 if err != nil { 1348 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1349 return 1350 } 1351 1352 err = db.NewIssue(tx, &db.Issue{ 1353 RepoAt: f.RepoAt, 1354 Title: title, 1355 Body: body, 1356 OwnerDid: user.Did, 1357 }) 1358 if err != nil { 1359 log.Println("failed to create issue", err) 1360 s.pages.Notice(w, "issues", "Failed to create issue.") 1361 return 1362 } 1363 1364 issueId, err := db.GetIssueId(s.db, f.RepoAt) 1365 if err != nil { 1366 log.Println("failed to get issue id", err) 1367 s.pages.Notice(w, "issues", "Failed to create issue.") 1368 return 1369 } 1370 1371 client, _ := s.auth.AuthorizedClient(r) 1372 atUri := f.RepoAt.String() 1373 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1374 Collection: tangled.RepoIssueNSID, 1375 Repo: user.Did, 1376 Rkey: s.TID(), 1377 Record: &lexutil.LexiconTypeDecoder{ 1378 Val: &tangled.RepoIssue{ 1379 Repo: atUri, 1380 Title: title, 1381 Body: &body, 1382 Owner: user.Did, 1383 IssueId: int64(issueId), 1384 }, 1385 }, 1386 }) 1387 if err != nil { 1388 log.Println("failed to create issue", err) 1389 s.pages.Notice(w, "issues", "Failed to create issue.") 1390 return 1391 } 1392 1393 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1394 if err != nil { 1395 log.Println("failed to set issue at", err) 1396 s.pages.Notice(w, "issues", "Failed to create issue.") 1397 return 1398 } 1399 1400 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1401 return 1402 } 1403} 1404 1405func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 1406 user := s.auth.GetUser(r) 1407 params := r.URL.Query() 1408 1409 state := db.PullOpen 1410 switch params.Get("state") { 1411 case "closed": 1412 state = db.PullClosed 1413 case "merged": 1414 state = db.PullMerged 1415 } 1416 1417 f, err := fullyResolvedRepo(r) 1418 if err != nil { 1419 log.Println("failed to get repo and knot", err) 1420 return 1421 } 1422 1423 pulls, err := db.GetPulls(s.db, f.RepoAt, state) 1424 if err != nil { 1425 log.Println("failed to get pulls", err) 1426 s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.") 1427 return 1428 } 1429 1430 identsToResolve := make([]string, len(pulls)) 1431 for i, pull := range pulls { 1432 identsToResolve[i] = pull.OwnerDid 1433 } 1434 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 1435 didHandleMap := make(map[string]string) 1436 for _, identity := range resolvedIds { 1437 if !identity.Handle.IsInvalidHandle() { 1438 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1439 } else { 1440 didHandleMap[identity.DID.String()] = identity.DID.String() 1441 } 1442 } 1443 1444 s.pages.RepoPulls(w, pages.RepoPullsParams{ 1445 LoggedInUser: s.auth.GetUser(r), 1446 RepoInfo: f.RepoInfo(s, user), 1447 Pulls: pulls, 1448 DidHandleMap: didHandleMap, 1449 FilteringBy: state, 1450 }) 1451 return 1452} 1453 1454func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 1455 repoName := chi.URLParam(r, "repo") 1456 knot, ok := r.Context().Value("knot").(string) 1457 if !ok { 1458 log.Println("malformed middleware") 1459 return nil, fmt.Errorf("malformed middleware") 1460 } 1461 id, ok := r.Context().Value("resolvedId").(identity.Identity) 1462 if !ok { 1463 log.Println("malformed middleware") 1464 return nil, fmt.Errorf("malformed middleware") 1465 } 1466 1467 repoAt, ok := r.Context().Value("repoAt").(string) 1468 if !ok { 1469 log.Println("malformed middleware") 1470 return nil, fmt.Errorf("malformed middleware") 1471 } 1472 1473 parsedRepoAt, err := syntax.ParseATURI(repoAt) 1474 if err != nil { 1475 log.Println("malformed repo at-uri") 1476 return nil, fmt.Errorf("malformed middleware") 1477 } 1478 1479 // pass through values from the middleware 1480 description, ok := r.Context().Value("repoDescription").(string) 1481 addedAt, ok := r.Context().Value("repoAddedAt").(string) 1482 1483 return &FullyResolvedRepo{ 1484 Knot: knot, 1485 OwnerId: id, 1486 RepoName: repoName, 1487 RepoAt: parsedRepoAt, 1488 Description: description, 1489 AddedAt: addedAt, 1490 }, nil 1491} 1492 1493func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 1494 if u != nil { 1495 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo()) 1496 return pages.RolesInRepo{r} 1497 } else { 1498 return pages.RolesInRepo{} 1499 } 1500}