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