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