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