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