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