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