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