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