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