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 "strconv" 13 "strings" 14 "time" 15 16 "github.com/bluesky-social/indigo/atproto/identity" 17 securejoin "github.com/cyphar/filepath-securejoin" 18 "github.com/go-chi/chi/v5" 19 "github.com/sotangled/tangled/api/tangled" 20 "github.com/sotangled/tangled/appview/auth" 21 "github.com/sotangled/tangled/appview/db" 22 "github.com/sotangled/tangled/appview/pages" 23 "github.com/sotangled/tangled/types" 24 25 comatproto "github.com/bluesky-social/indigo/api/atproto" 26 lexutil "github.com/bluesky-social/indigo/lex/util" 27) 28 29func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 30 ref := chi.URLParam(r, "ref") 31 f, err := fullyResolvedRepo(r) 32 if err != nil { 33 log.Println("failed to fully resolve repo", err) 34 return 35 } 36 var reqUrl string 37 if ref != "" { 38 reqUrl = fmt.Sprintf("http://%s/%s/%s/tree/%s", f.Knot, f.OwnerDid(), f.RepoName, ref) 39 } else { 40 reqUrl = fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName) 41 } 42 43 resp, err := http.Get(reqUrl) 44 if err != nil { 45 s.pages.Error503(w) 46 log.Println("failed to reach knotserver", err) 47 return 48 } 49 defer resp.Body.Close() 50 51 body, err := io.ReadAll(resp.Body) 52 if err != nil { 53 log.Fatalf("Error reading response body: %v", err) 54 return 55 } 56 57 var result types.RepoIndexResponse 58 err = json.Unmarshal(body, &result) 59 if err != nil { 60 log.Fatalf("Error unmarshalling response body: %v", err) 61 return 62 } 63 64 tagMap := make(map[string][]string) 65 for _, tag := range result.Tags { 66 hash := tag.Hash 67 tagMap[hash] = append(tagMap[hash], tag.Name) 68 } 69 70 for _, branch := range result.Branches { 71 hash := branch.Hash 72 tagMap[hash] = append(tagMap[hash], branch.Name) 73 } 74 75 user := s.auth.GetUser(r) 76 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 77 LoggedInUser: user, 78 RepoInfo: pages.RepoInfo{ 79 OwnerDid: f.OwnerDid(), 80 OwnerHandle: f.OwnerHandle(), 81 Name: f.RepoName, 82 SettingsAllowed: settingsAllowed(s, user, f), 83 }, 84 TagMap: tagMap, 85 RepoIndexResponse: result, 86 }) 87 88 return 89} 90 91func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 92 f, err := fullyResolvedRepo(r) 93 if err != nil { 94 log.Println("failed to fully resolve repo", err) 95 return 96 } 97 98 page := 1 99 if r.URL.Query().Get("page") != "" { 100 page, err = strconv.Atoi(r.URL.Query().Get("page")) 101 if err != nil { 102 page = 1 103 } 104 } 105 106 ref := chi.URLParam(r, "ref") 107 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s?page=%d&per_page=30", f.Knot, f.OwnerDid(), f.RepoName, ref, page)) 108 if err != nil { 109 log.Println("failed to reach knotserver", err) 110 return 111 } 112 113 body, err := io.ReadAll(resp.Body) 114 if err != nil { 115 log.Printf("error reading response body: %v", err) 116 return 117 } 118 119 var repolog types.RepoLogResponse 120 err = json.Unmarshal(body, &repolog) 121 if err != nil { 122 log.Println("failed to parse json response", err) 123 return 124 } 125 126 user := s.auth.GetUser(r) 127 s.pages.RepoLog(w, pages.RepoLogParams{ 128 LoggedInUser: user, 129 RepoInfo: pages.RepoInfo{ 130 OwnerDid: f.OwnerDid(), 131 OwnerHandle: f.OwnerHandle(), 132 Name: f.RepoName, 133 SettingsAllowed: settingsAllowed(s, user, f), 134 }, 135 RepoLogResponse: repolog, 136 }) 137 return 138} 139 140func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 141 f, err := fullyResolvedRepo(r) 142 if err != nil { 143 log.Println("failed to fully resolve repo", err) 144 return 145 } 146 147 ref := chi.URLParam(r, "ref") 148 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref)) 149 if err != nil { 150 log.Println("failed to reach knotserver", err) 151 return 152 } 153 154 body, err := io.ReadAll(resp.Body) 155 if err != nil { 156 log.Fatalf("Error reading response body: %v", err) 157 return 158 } 159 160 var result types.RepoCommitResponse 161 err = json.Unmarshal(body, &result) 162 if err != nil { 163 log.Println("failed to parse response:", err) 164 return 165 } 166 167 user := s.auth.GetUser(r) 168 s.pages.RepoCommit(w, pages.RepoCommitParams{ 169 LoggedInUser: user, 170 RepoInfo: pages.RepoInfo{ 171 OwnerDid: f.OwnerDid(), 172 OwnerHandle: f.OwnerHandle(), 173 Name: f.RepoName, 174 SettingsAllowed: settingsAllowed(s, user, f), 175 }, 176 RepoCommitResponse: result, 177 }) 178 return 179} 180 181func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 182 f, err := fullyResolvedRepo(r) 183 if err != nil { 184 log.Println("failed to fully resolve repo", err) 185 return 186 } 187 188 ref := chi.URLParam(r, "ref") 189 treePath := chi.URLParam(r, "*") 190 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 191 if err != nil { 192 log.Println("failed to reach knotserver", err) 193 return 194 } 195 196 body, err := io.ReadAll(resp.Body) 197 if err != nil { 198 log.Fatalf("Error reading response body: %v", err) 199 return 200 } 201 202 var result types.RepoTreeResponse 203 err = json.Unmarshal(body, &result) 204 if err != nil { 205 log.Println("failed to parse response:", err) 206 return 207 } 208 209 user := s.auth.GetUser(r) 210 211 var breadcrumbs [][]string 212 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 213 if treePath != "" { 214 for idx, elem := range strings.Split(treePath, "/") { 215 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 216 } 217 } 218 219 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath) 220 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath) 221 222 log.Println(result) 223 224 s.pages.RepoTree(w, pages.RepoTreeParams{ 225 LoggedInUser: user, 226 BreadCrumbs: breadcrumbs, 227 BaseTreeLink: baseTreeLink, 228 BaseBlobLink: baseBlobLink, 229 RepoInfo: pages.RepoInfo{ 230 OwnerDid: f.OwnerDid(), 231 OwnerHandle: f.OwnerHandle(), 232 Name: f.RepoName, 233 SettingsAllowed: settingsAllowed(s, user, f), 234 }, 235 RepoTreeResponse: result, 236 }) 237 return 238} 239 240func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 241 f, err := fullyResolvedRepo(r) 242 if err != nil { 243 log.Println("failed to get repo and knot", err) 244 return 245 } 246 247 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName)) 248 if err != nil { 249 log.Println("failed to reach knotserver", err) 250 return 251 } 252 253 body, err := io.ReadAll(resp.Body) 254 if err != nil { 255 log.Fatalf("Error reading response body: %v", err) 256 return 257 } 258 259 var result types.RepoTagsResponse 260 err = json.Unmarshal(body, &result) 261 if err != nil { 262 log.Println("failed to parse response:", err) 263 return 264 } 265 266 user := s.auth.GetUser(r) 267 s.pages.RepoTags(w, pages.RepoTagsParams{ 268 LoggedInUser: user, 269 RepoInfo: pages.RepoInfo{ 270 OwnerDid: f.OwnerDid(), 271 OwnerHandle: f.OwnerHandle(), 272 Name: f.RepoName, 273 SettingsAllowed: settingsAllowed(s, user, f), 274 }, 275 RepoTagsResponse: result, 276 }) 277 return 278} 279 280func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 281 f, err := fullyResolvedRepo(r) 282 if err != nil { 283 log.Println("failed to get repo and knot", err) 284 return 285 } 286 287 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName)) 288 if err != nil { 289 log.Println("failed to reach knotserver", err) 290 return 291 } 292 293 body, err := io.ReadAll(resp.Body) 294 if err != nil { 295 log.Fatalf("Error reading response body: %v", err) 296 return 297 } 298 299 var result types.RepoBranchesResponse 300 err = json.Unmarshal(body, &result) 301 if err != nil { 302 log.Println("failed to parse response:", err) 303 return 304 } 305 306 user := s.auth.GetUser(r) 307 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 308 LoggedInUser: user, 309 RepoInfo: pages.RepoInfo{ 310 OwnerDid: f.OwnerDid(), 311 OwnerHandle: f.OwnerHandle(), 312 Name: f.RepoName, 313 SettingsAllowed: settingsAllowed(s, user, f), 314 }, 315 RepoBranchesResponse: result, 316 }) 317 return 318} 319 320func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 321 f, err := fullyResolvedRepo(r) 322 if err != nil { 323 log.Println("failed to get repo and knot", err) 324 return 325 } 326 327 ref := chi.URLParam(r, "ref") 328 filePath := chi.URLParam(r, "*") 329 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 330 if err != nil { 331 log.Println("failed to reach knotserver", err) 332 return 333 } 334 335 body, err := io.ReadAll(resp.Body) 336 if err != nil { 337 log.Fatalf("Error reading response body: %v", err) 338 return 339 } 340 341 var result types.RepoBlobResponse 342 err = json.Unmarshal(body, &result) 343 if err != nil { 344 log.Println("failed to parse response:", err) 345 return 346 } 347 348 var breadcrumbs [][]string 349 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)}) 350 if filePath != "" { 351 for idx, elem := range strings.Split(filePath, "/") { 352 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 353 } 354 } 355 356 user := s.auth.GetUser(r) 357 s.pages.RepoBlob(w, pages.RepoBlobParams{ 358 LoggedInUser: user, 359 RepoInfo: pages.RepoInfo{ 360 OwnerDid: f.OwnerDid(), 361 OwnerHandle: f.OwnerHandle(), 362 Name: f.RepoName, 363 SettingsAllowed: settingsAllowed(s, user, f), 364 }, 365 RepoBlobResponse: result, 366 BreadCrumbs: breadcrumbs, 367 }) 368 return 369} 370 371func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 372 f, err := fullyResolvedRepo(r) 373 if err != nil { 374 log.Println("failed to get repo and knot", err) 375 return 376 } 377 378 collaborator := r.FormValue("collaborator") 379 if collaborator == "" { 380 http.Error(w, "malformed form", http.StatusBadRequest) 381 return 382 } 383 384 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator) 385 if err != nil { 386 w.Write([]byte("failed to resolve collaborator did to a handle")) 387 return 388 } 389 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 390 391 // TODO: create an atproto record for this 392 393 secret, err := s.db.GetRegistrationKey(f.Knot) 394 if err != nil { 395 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 396 return 397 } 398 399 ksClient, err := NewSignedClient(f.Knot, secret) 400 if err != nil { 401 log.Println("failed to create client to ", f.Knot) 402 return 403 } 404 405 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 406 if err != nil { 407 log.Printf("failed to make request to %s: %s", f.Knot, err) 408 return 409 } 410 411 if ksResp.StatusCode != http.StatusNoContent { 412 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err))) 413 return 414 } 415 416 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo()) 417 if err != nil { 418 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 419 return 420 } 421 422 err = s.db.AddCollaborator(collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 423 if err != nil { 424 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 425 return 426 } 427 428 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 429 430} 431 432func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 433 f, err := fullyResolvedRepo(r) 434 if err != nil { 435 log.Println("failed to get repo and knot", err) 436 return 437 } 438 439 switch r.Method { 440 case http.MethodGet: 441 // for now, this is just pubkeys 442 user := s.auth.GetUser(r) 443 repoCollaborators, err := f.Collaborators(r.Context(), s) 444 if err != nil { 445 log.Println("failed to get collaborators", err) 446 } 447 448 isCollaboratorInviteAllowed := false 449 if user != nil { 450 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo()) 451 if err == nil && ok { 452 isCollaboratorInviteAllowed = true 453 } 454 } 455 456 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 457 LoggedInUser: user, 458 RepoInfo: pages.RepoInfo{ 459 OwnerDid: f.OwnerDid(), 460 OwnerHandle: f.OwnerHandle(), 461 Name: f.RepoName, 462 SettingsAllowed: settingsAllowed(s, user, f), 463 }, 464 Collaborators: repoCollaborators, 465 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 466 }) 467 } 468} 469 470type FullyResolvedRepo struct { 471 Knot string 472 OwnerId identity.Identity 473 RepoName string 474 RepoAt string 475} 476 477func (f *FullyResolvedRepo) OwnerDid() string { 478 return f.OwnerId.DID.String() 479} 480 481func (f *FullyResolvedRepo) OwnerHandle() string { 482 return f.OwnerId.Handle.String() 483} 484 485func (f *FullyResolvedRepo) OwnerSlashRepo() string { 486 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 487 return p 488} 489 490func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 491 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) 492 if err != nil { 493 return nil, err 494 } 495 496 var collaborators []pages.Collaborator 497 for _, item := range repoCollaborators { 498 // currently only two roles: owner and member 499 var role string 500 if item[3] == "repo:owner" { 501 role = "owner" 502 } else if item[3] == "repo:collaborator" { 503 role = "collaborator" 504 } else { 505 continue 506 } 507 508 did := item[0] 509 510 c := pages.Collaborator{ 511 Did: did, 512 Handle: "", 513 Role: role, 514 } 515 collaborators = append(collaborators, c) 516 } 517 518 // populate all collborators with handles 519 identsToResolve := make([]string, len(collaborators)) 520 for i, collab := range collaborators { 521 identsToResolve[i] = collab.Did 522 } 523 524 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 525 for i, resolved := range resolvedIdents { 526 if resolved != nil { 527 collaborators[i].Handle = resolved.Handle.String() 528 } 529 } 530 531 return collaborators, nil 532} 533 534func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 535 user := s.auth.GetUser(r) 536 f, err := fullyResolvedRepo(r) 537 if err != nil { 538 log.Println("failed to get repo and knot", err) 539 return 540 } 541 542 issueId := chi.URLParam(r, "issue") 543 issueIdInt, err := strconv.Atoi(issueId) 544 if err != nil { 545 http.Error(w, "bad issue id", http.StatusBadRequest) 546 log.Println("failed to parse issue id", err) 547 return 548 } 549 550 issue, comments, err := s.db.GetIssueWithComments(f.RepoAt, issueIdInt) 551 if err != nil { 552 log.Println("failed to get issue and comments", err) 553 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 554 return 555 } 556 557 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 558 if err != nil { 559 log.Println("failed to resolve issue owner", err) 560 } 561 562 identsToResolve := make([]string, len(comments)) 563 for i, comment := range comments { 564 identsToResolve[i] = comment.OwnerDid 565 } 566 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 567 didHandleMap := make(map[string]string) 568 for _, identity := range resolvedIds { 569 if !identity.Handle.IsInvalidHandle() { 570 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 571 } else { 572 didHandleMap[identity.DID.String()] = identity.DID.String() 573 } 574 } 575 576 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 577 LoggedInUser: user, 578 RepoInfo: pages.RepoInfo{ 579 OwnerDid: f.OwnerDid(), 580 OwnerHandle: f.OwnerHandle(), 581 Name: f.RepoName, 582 SettingsAllowed: settingsAllowed(s, user, f), 583 }, 584 Issue: *issue, 585 Comments: comments, 586 587 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 588 DidHandleMap: didHandleMap, 589 }) 590 591} 592 593func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 594 user := s.auth.GetUser(r) 595 f, err := fullyResolvedRepo(r) 596 if err != nil { 597 log.Println("failed to get repo and knot", err) 598 return 599 } 600 601 issueId := chi.URLParam(r, "issue") 602 issueIdInt, err := strconv.Atoi(issueId) 603 if err != nil { 604 http.Error(w, "bad issue id", http.StatusBadRequest) 605 log.Println("failed to parse issue id", err) 606 return 607 } 608 609 issue, err := s.db.GetIssue(f.RepoAt, issueIdInt) 610 if err != nil { 611 log.Println("failed to get issue", err) 612 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 613 return 614 } 615 616 // TODO: make this more granular 617 if user.Did == f.OwnerDid() { 618 619 closed := tangled.RepoIssueStateClosed 620 621 client, _ := s.auth.AuthorizedClient(r) 622 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 623 Collection: tangled.RepoIssueStateNSID, 624 Repo: issue.OwnerDid, 625 Rkey: s.TID(), 626 Record: &lexutil.LexiconTypeDecoder{ 627 Val: &tangled.RepoIssueState{ 628 Issue: issue.IssueAt, 629 State: &closed, 630 }, 631 }, 632 }) 633 634 if err != nil { 635 log.Println("failed to update issue state", err) 636 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 637 return 638 } 639 640 err := s.db.CloseIssue(f.RepoAt, issueIdInt) 641 if err != nil { 642 log.Println("failed to close issue", err) 643 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 644 return 645 } 646 647 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 648 return 649 } else { 650 log.Println("user is not the owner of the repo") 651 http.Error(w, "for biden", http.StatusUnauthorized) 652 return 653 } 654} 655 656func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 657 user := s.auth.GetUser(r) 658 f, err := fullyResolvedRepo(r) 659 if err != nil { 660 log.Println("failed to get repo and knot", err) 661 return 662 } 663 664 issueId := chi.URLParam(r, "issue") 665 issueIdInt, err := strconv.Atoi(issueId) 666 if err != nil { 667 http.Error(w, "bad issue id", http.StatusBadRequest) 668 log.Println("failed to parse issue id", err) 669 return 670 } 671 672 if user.Did == f.OwnerDid() { 673 err := s.db.ReopenIssue(f.RepoAt, issueIdInt) 674 if err != nil { 675 log.Println("failed to reopen issue", err) 676 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 677 return 678 } 679 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 680 return 681 } else { 682 log.Println("user is not the owner of the repo") 683 http.Error(w, "forbidden", http.StatusUnauthorized) 684 return 685 } 686} 687 688func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 689 user := s.auth.GetUser(r) 690 f, err := fullyResolvedRepo(r) 691 if err != nil { 692 log.Println("failed to get repo and knot", err) 693 return 694 } 695 696 issueId := chi.URLParam(r, "issue") 697 issueIdInt, err := strconv.Atoi(issueId) 698 if err != nil { 699 http.Error(w, "bad issue id", http.StatusBadRequest) 700 log.Println("failed to parse issue id", err) 701 return 702 } 703 704 switch r.Method { 705 case http.MethodPost: 706 body := r.FormValue("body") 707 if body == "" { 708 s.pages.Notice(w, "issue", "Body is required") 709 return 710 } 711 712 commentId := rand.IntN(1000000) 713 714 err := s.db.NewComment(&db.Comment{ 715 OwnerDid: user.Did, 716 RepoAt: f.RepoAt, 717 Issue: issueIdInt, 718 CommentId: commentId, 719 Body: body, 720 }) 721 if err != nil { 722 log.Println("failed to create comment", err) 723 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 724 return 725 } 726 727 createdAt := time.Now().Format(time.RFC3339) 728 commentIdInt64 := int64(commentId) 729 ownerDid := user.Did 730 issueAt, err := s.db.GetIssueAt(f.RepoAt, issueIdInt) 731 if err != nil { 732 log.Println("failed to get issue at", err) 733 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 734 return 735 } 736 737 client, _ := s.auth.AuthorizedClient(r) 738 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 739 Collection: tangled.RepoIssueCommentNSID, 740 Repo: user.Did, 741 Rkey: s.TID(), 742 Record: &lexutil.LexiconTypeDecoder{ 743 Val: &tangled.RepoIssueComment{ 744 Repo: &f.RepoAt, 745 Issue: issueAt, 746 CommentId: &commentIdInt64, 747 Owner: &ownerDid, 748 Body: &body, 749 CreatedAt: &createdAt, 750 }, 751 }, 752 }) 753 if err != nil { 754 log.Println("failed to create comment", err) 755 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 756 return 757 } 758 759 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 760 return 761 } 762} 763 764func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 765 user := s.auth.GetUser(r) 766 f, err := fullyResolvedRepo(r) 767 if err != nil { 768 log.Println("failed to get repo and knot", err) 769 return 770 } 771 772 issues, err := s.db.GetIssues(f.RepoAt) 773 if err != nil { 774 log.Println("failed to get issues", err) 775 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 776 return 777 } 778 779 identsToResolve := make([]string, len(issues)) 780 for i, issue := range issues { 781 identsToResolve[i] = issue.OwnerDid 782 } 783 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 784 didHandleMap := make(map[string]string) 785 for _, identity := range resolvedIds { 786 if !identity.Handle.IsInvalidHandle() { 787 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 788 } else { 789 didHandleMap[identity.DID.String()] = identity.DID.String() 790 } 791 } 792 793 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 794 LoggedInUser: s.auth.GetUser(r), 795 RepoInfo: pages.RepoInfo{ 796 OwnerDid: f.OwnerDid(), 797 OwnerHandle: f.OwnerHandle(), 798 Name: f.RepoName, 799 SettingsAllowed: settingsAllowed(s, user, f), 800 }, 801 Issues: issues, 802 DidHandleMap: didHandleMap, 803 }) 804 return 805} 806 807func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 808 user := s.auth.GetUser(r) 809 810 f, err := fullyResolvedRepo(r) 811 if err != nil { 812 log.Println("failed to get repo and knot", err) 813 return 814 } 815 816 switch r.Method { 817 case http.MethodGet: 818 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 819 LoggedInUser: user, 820 RepoInfo: pages.RepoInfo{ 821 Name: f.RepoName, 822 OwnerDid: f.OwnerDid(), 823 OwnerHandle: f.OwnerHandle(), 824 SettingsAllowed: settingsAllowed(s, user, f), 825 }, 826 }) 827 case http.MethodPost: 828 title := r.FormValue("title") 829 body := r.FormValue("body") 830 831 if title == "" || body == "" { 832 s.pages.Notice(w, "issues", "Title and body are required") 833 return 834 } 835 836 err = s.db.NewIssue(&db.Issue{ 837 RepoAt: f.RepoAt, 838 Title: title, 839 Body: body, 840 OwnerDid: user.Did, 841 }) 842 if err != nil { 843 log.Println("failed to create issue", err) 844 s.pages.Notice(w, "issues", "Failed to create issue.") 845 return 846 } 847 848 issueId, err := s.db.GetIssueId(f.RepoAt) 849 if err != nil { 850 log.Println("failed to get issue id", err) 851 s.pages.Notice(w, "issues", "Failed to create issue.") 852 return 853 } 854 855 client, _ := s.auth.AuthorizedClient(r) 856 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 857 Collection: tangled.RepoIssueNSID, 858 Repo: user.Did, 859 Rkey: s.TID(), 860 Record: &lexutil.LexiconTypeDecoder{ 861 Val: &tangled.RepoIssue{ 862 Repo: f.RepoAt, 863 Title: title, 864 Body: &body, 865 Owner: user.Did, 866 IssueId: int64(issueId), 867 }, 868 }, 869 }) 870 if err != nil { 871 log.Println("failed to create issue", err) 872 s.pages.Notice(w, "issues", "Failed to create issue.") 873 return 874 } 875 876 err = s.db.SetIssueAt(f.RepoAt, issueId, resp.Uri) 877 if err != nil { 878 log.Println("failed to set issue at", err) 879 s.pages.Notice(w, "issues", "Failed to create issue.") 880 return 881 } 882 883 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 884 return 885 } 886} 887 888func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 889 user := s.auth.GetUser(r) 890 f, err := fullyResolvedRepo(r) 891 if err != nil { 892 log.Println("failed to get repo and knot", err) 893 return 894 } 895 896 switch r.Method { 897 case http.MethodGet: 898 s.pages.RepoPulls(w, pages.RepoPullsParams{ 899 LoggedInUser: user, 900 RepoInfo: pages.RepoInfo{ 901 Name: f.RepoName, 902 OwnerDid: f.OwnerDid(), 903 OwnerHandle: f.OwnerHandle(), 904 SettingsAllowed: settingsAllowed(s, user, f), 905 }, 906 }) 907 } 908} 909 910func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 911 repoName := chi.URLParam(r, "repo") 912 knot, ok := r.Context().Value("knot").(string) 913 if !ok { 914 log.Println("malformed middleware") 915 return nil, fmt.Errorf("malformed middleware") 916 } 917 id, ok := r.Context().Value("resolvedId").(identity.Identity) 918 if !ok { 919 log.Println("malformed middleware") 920 return nil, fmt.Errorf("malformed middleware") 921 } 922 923 repoAt, ok := r.Context().Value("repoAt").(string) 924 if !ok { 925 log.Println("malformed middleware") 926 return nil, fmt.Errorf("malformed middleware") 927 } 928 929 return &FullyResolvedRepo{ 930 Knot: knot, 931 OwnerId: id, 932 RepoName: repoName, 933 RepoAt: repoAt, 934 }, nil 935} 936 937func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool { 938 settingsAllowed := false 939 if u != nil { 940 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo()) 941 if err == nil && ok { 942 settingsAllowed = true 943 } else { 944 log.Println(err, ok) 945 } 946 } 947 948 return settingsAllowed 949}