forked from tangled.org/core
this repo has no description
1package state 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "math/rand/v2" 10 "net/http" 11 "path" 12 "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 := db.GetRegistrationKey(s.db, 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, s.config.Dev) 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 tx, err := s.db.BeginTx(r.Context(), nil) 416 if err != nil { 417 log.Println("failed to start tx") 418 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 419 return 420 } 421 defer func() { 422 tx.Rollback() 423 err = s.enforcer.E.LoadPolicy() 424 if err != nil { 425 log.Println("failed to rollback policies") 426 } 427 }() 428 429 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo()) 430 if err != nil { 431 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 432 return 433 } 434 435 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 436 if err != nil { 437 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 438 return 439 } 440 441 err = tx.Commit() 442 if err != nil { 443 log.Println("failed to commit changes", err) 444 http.Error(w, err.Error(), http.StatusInternalServerError) 445 return 446 } 447 448 err = s.enforcer.E.SavePolicy() 449 if err != nil { 450 log.Println("failed to update ACLs", err) 451 http.Error(w, err.Error(), http.StatusInternalServerError) 452 return 453 } 454 455 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String()))) 456 457} 458 459func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 460 f, err := fullyResolvedRepo(r) 461 if err != nil { 462 log.Println("failed to get repo and knot", err) 463 return 464 } 465 466 switch r.Method { 467 case http.MethodGet: 468 // for now, this is just pubkeys 469 user := s.auth.GetUser(r) 470 repoCollaborators, err := f.Collaborators(r.Context(), s) 471 if err != nil { 472 log.Println("failed to get collaborators", err) 473 } 474 475 isCollaboratorInviteAllowed := false 476 if user != nil { 477 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo()) 478 if err == nil && ok { 479 isCollaboratorInviteAllowed = true 480 } 481 } 482 483 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 484 LoggedInUser: user, 485 RepoInfo: pages.RepoInfo{ 486 OwnerDid: f.OwnerDid(), 487 OwnerHandle: f.OwnerHandle(), 488 Name: f.RepoName, 489 SettingsAllowed: settingsAllowed(s, user, f), 490 }, 491 Collaborators: repoCollaborators, 492 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 493 }) 494 } 495} 496 497type FullyResolvedRepo struct { 498 Knot string 499 OwnerId identity.Identity 500 RepoName string 501 RepoAt string 502} 503 504func (f *FullyResolvedRepo) OwnerDid() string { 505 return f.OwnerId.DID.String() 506} 507 508func (f *FullyResolvedRepo) OwnerHandle() string { 509 return f.OwnerId.Handle.String() 510} 511 512func (f *FullyResolvedRepo) OwnerSlashRepo() string { 513 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 514 return p 515} 516 517func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) { 518 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) 519 if err != nil { 520 return nil, err 521 } 522 523 var collaborators []pages.Collaborator 524 for _, item := range repoCollaborators { 525 // currently only two roles: owner and member 526 var role string 527 if item[3] == "repo:owner" { 528 role = "owner" 529 } else if item[3] == "repo:collaborator" { 530 role = "collaborator" 531 } else { 532 continue 533 } 534 535 did := item[0] 536 537 c := pages.Collaborator{ 538 Did: did, 539 Handle: "", 540 Role: role, 541 } 542 collaborators = append(collaborators, c) 543 } 544 545 // populate all collborators with handles 546 identsToResolve := make([]string, len(collaborators)) 547 for i, collab := range collaborators { 548 identsToResolve[i] = collab.Did 549 } 550 551 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve) 552 for i, resolved := range resolvedIdents { 553 if resolved != nil { 554 collaborators[i].Handle = resolved.Handle.String() 555 } 556 } 557 558 return collaborators, nil 559} 560 561func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 562 user := s.auth.GetUser(r) 563 f, err := fullyResolvedRepo(r) 564 if err != nil { 565 log.Println("failed to get repo and knot", err) 566 return 567 } 568 569 issueId := chi.URLParam(r, "issue") 570 issueIdInt, err := strconv.Atoi(issueId) 571 if err != nil { 572 http.Error(w, "bad issue id", http.StatusBadRequest) 573 log.Println("failed to parse issue id", err) 574 return 575 } 576 577 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 578 if err != nil { 579 log.Println("failed to get issue and comments", err) 580 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 581 return 582 } 583 584 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid) 585 if err != nil { 586 log.Println("failed to resolve issue owner", err) 587 } 588 589 identsToResolve := make([]string, len(comments)) 590 for i, comment := range comments { 591 identsToResolve[i] = comment.OwnerDid 592 } 593 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 594 didHandleMap := make(map[string]string) 595 for _, identity := range resolvedIds { 596 if !identity.Handle.IsInvalidHandle() { 597 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 598 } else { 599 didHandleMap[identity.DID.String()] = identity.DID.String() 600 } 601 } 602 603 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 604 LoggedInUser: user, 605 RepoInfo: pages.RepoInfo{ 606 OwnerDid: f.OwnerDid(), 607 OwnerHandle: f.OwnerHandle(), 608 Name: f.RepoName, 609 SettingsAllowed: settingsAllowed(s, user, f), 610 }, 611 Issue: *issue, 612 Comments: comments, 613 614 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 615 DidHandleMap: didHandleMap, 616 }) 617 618} 619 620func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 621 user := s.auth.GetUser(r) 622 f, err := fullyResolvedRepo(r) 623 if err != nil { 624 log.Println("failed to get repo and knot", err) 625 return 626 } 627 628 issueId := chi.URLParam(r, "issue") 629 issueIdInt, err := strconv.Atoi(issueId) 630 if err != nil { 631 http.Error(w, "bad issue id", http.StatusBadRequest) 632 log.Println("failed to parse issue id", err) 633 return 634 } 635 636 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 637 if err != nil { 638 log.Println("failed to get issue", err) 639 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 640 return 641 } 642 643 collaborators, err := f.Collaborators(r.Context(), s) 644 if err != nil { 645 log.Println("failed to fetch repo collaborators: %w", err) 646 } 647 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 648 return user.Did == collab.Did 649 }) 650 isIssueOwner := user.Did == issue.OwnerDid 651 652 // TODO: make this more granular 653 if isIssueOwner || isCollaborator { 654 655 closed := tangled.RepoIssueStateClosed 656 657 client, _ := s.auth.AuthorizedClient(r) 658 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 659 Collection: tangled.RepoIssueStateNSID, 660 Repo: issue.OwnerDid, 661 Rkey: s.TID(), 662 Record: &lexutil.LexiconTypeDecoder{ 663 Val: &tangled.RepoIssueState{ 664 Issue: issue.IssueAt, 665 State: &closed, 666 }, 667 }, 668 }) 669 670 if err != nil { 671 log.Println("failed to update issue state", err) 672 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 673 return 674 } 675 676 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 677 if err != nil { 678 log.Println("failed to close issue", err) 679 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 680 return 681 } 682 683 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 684 return 685 } else { 686 log.Println("user is not permitted to close issue") 687 http.Error(w, "for biden", http.StatusUnauthorized) 688 return 689 } 690} 691 692func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 693 user := s.auth.GetUser(r) 694 f, err := fullyResolvedRepo(r) 695 if err != nil { 696 log.Println("failed to get repo and knot", err) 697 return 698 } 699 700 issueId := chi.URLParam(r, "issue") 701 issueIdInt, err := strconv.Atoi(issueId) 702 if err != nil { 703 http.Error(w, "bad issue id", http.StatusBadRequest) 704 log.Println("failed to parse issue id", err) 705 return 706 } 707 708 if user.Did == f.OwnerDid() { 709 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 710 if err != nil { 711 log.Println("failed to reopen issue", err) 712 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 713 return 714 } 715 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 716 return 717 } else { 718 log.Println("user is not the owner of the repo") 719 http.Error(w, "forbidden", http.StatusUnauthorized) 720 return 721 } 722} 723 724func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 725 user := s.auth.GetUser(r) 726 f, err := fullyResolvedRepo(r) 727 if err != nil { 728 log.Println("failed to get repo and knot", err) 729 return 730 } 731 732 issueId := chi.URLParam(r, "issue") 733 issueIdInt, err := strconv.Atoi(issueId) 734 if err != nil { 735 http.Error(w, "bad issue id", http.StatusBadRequest) 736 log.Println("failed to parse issue id", err) 737 return 738 } 739 740 switch r.Method { 741 case http.MethodPost: 742 body := r.FormValue("body") 743 if body == "" { 744 s.pages.Notice(w, "issue", "Body is required") 745 return 746 } 747 748 commentId := rand.IntN(1000000) 749 750 err := db.NewComment(s.db, &db.Comment{ 751 OwnerDid: user.Did, 752 RepoAt: f.RepoAt, 753 Issue: issueIdInt, 754 CommentId: commentId, 755 Body: body, 756 }) 757 if err != nil { 758 log.Println("failed to create comment", err) 759 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 760 return 761 } 762 763 createdAt := time.Now().Format(time.RFC3339) 764 commentIdInt64 := int64(commentId) 765 ownerDid := user.Did 766 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 767 if err != nil { 768 log.Println("failed to get issue at", err) 769 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 770 return 771 } 772 773 client, _ := s.auth.AuthorizedClient(r) 774 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 775 Collection: tangled.RepoIssueCommentNSID, 776 Repo: user.Did, 777 Rkey: s.TID(), 778 Record: &lexutil.LexiconTypeDecoder{ 779 Val: &tangled.RepoIssueComment{ 780 Repo: &f.RepoAt, 781 Issue: issueAt, 782 CommentId: &commentIdInt64, 783 Owner: &ownerDid, 784 Body: &body, 785 CreatedAt: &createdAt, 786 }, 787 }, 788 }) 789 if err != nil { 790 log.Println("failed to create comment", err) 791 s.pages.Notice(w, "issue-comment", "Failed to create comment.") 792 return 793 } 794 795 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 796 return 797 } 798} 799 800func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 801 user := s.auth.GetUser(r) 802 f, err := fullyResolvedRepo(r) 803 if err != nil { 804 log.Println("failed to get repo and knot", err) 805 return 806 } 807 808 issues, err := db.GetIssues(s.db, f.RepoAt) 809 if err != nil { 810 log.Println("failed to get issues", err) 811 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 812 return 813 } 814 815 identsToResolve := make([]string, len(issues)) 816 for i, issue := range issues { 817 identsToResolve[i] = issue.OwnerDid 818 } 819 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve) 820 didHandleMap := make(map[string]string) 821 for _, identity := range resolvedIds { 822 if !identity.Handle.IsInvalidHandle() { 823 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 824 } else { 825 didHandleMap[identity.DID.String()] = identity.DID.String() 826 } 827 } 828 829 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 830 LoggedInUser: s.auth.GetUser(r), 831 RepoInfo: pages.RepoInfo{ 832 OwnerDid: f.OwnerDid(), 833 OwnerHandle: f.OwnerHandle(), 834 Name: f.RepoName, 835 SettingsAllowed: settingsAllowed(s, user, f), 836 }, 837 Issues: issues, 838 DidHandleMap: didHandleMap, 839 }) 840 return 841} 842 843func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 844 user := s.auth.GetUser(r) 845 846 f, err := fullyResolvedRepo(r) 847 if err != nil { 848 log.Println("failed to get repo and knot", err) 849 return 850 } 851 852 switch r.Method { 853 case http.MethodGet: 854 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 855 LoggedInUser: user, 856 RepoInfo: pages.RepoInfo{ 857 Name: f.RepoName, 858 OwnerDid: f.OwnerDid(), 859 OwnerHandle: f.OwnerHandle(), 860 SettingsAllowed: settingsAllowed(s, user, f), 861 }, 862 }) 863 case http.MethodPost: 864 title := r.FormValue("title") 865 body := r.FormValue("body") 866 867 if title == "" || body == "" { 868 s.pages.Notice(w, "issues", "Title and body are required") 869 return 870 } 871 872 tx, err := s.db.BeginTx(r.Context(), nil) 873 if err != nil { 874 s.pages.Notice(w, "issues", "Failed to create issue, try again later") 875 return 876 } 877 878 err = db.NewIssue(tx, &db.Issue{ 879 RepoAt: f.RepoAt, 880 Title: title, 881 Body: body, 882 OwnerDid: user.Did, 883 }) 884 if err != nil { 885 log.Println("failed to create issue", err) 886 s.pages.Notice(w, "issues", "Failed to create issue.") 887 return 888 } 889 890 issueId, err := db.GetIssueId(s.db, f.RepoAt) 891 if err != nil { 892 log.Println("failed to get issue id", err) 893 s.pages.Notice(w, "issues", "Failed to create issue.") 894 return 895 } 896 897 client, _ := s.auth.AuthorizedClient(r) 898 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 899 Collection: tangled.RepoIssueNSID, 900 Repo: user.Did, 901 Rkey: s.TID(), 902 Record: &lexutil.LexiconTypeDecoder{ 903 Val: &tangled.RepoIssue{ 904 Repo: f.RepoAt, 905 Title: title, 906 Body: &body, 907 Owner: user.Did, 908 IssueId: int64(issueId), 909 }, 910 }, 911 }) 912 if err != nil { 913 log.Println("failed to create issue", err) 914 s.pages.Notice(w, "issues", "Failed to create issue.") 915 return 916 } 917 918 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 919 if err != nil { 920 log.Println("failed to set issue at", err) 921 s.pages.Notice(w, "issues", "Failed to create issue.") 922 return 923 } 924 925 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 926 return 927 } 928} 929 930func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 931 user := s.auth.GetUser(r) 932 f, err := fullyResolvedRepo(r) 933 if err != nil { 934 log.Println("failed to get repo and knot", err) 935 return 936 } 937 938 switch r.Method { 939 case http.MethodGet: 940 s.pages.RepoPulls(w, pages.RepoPullsParams{ 941 LoggedInUser: user, 942 RepoInfo: pages.RepoInfo{ 943 Name: f.RepoName, 944 OwnerDid: f.OwnerDid(), 945 OwnerHandle: f.OwnerHandle(), 946 SettingsAllowed: settingsAllowed(s, user, f), 947 }, 948 }) 949 } 950} 951 952func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 953 repoName := chi.URLParam(r, "repo") 954 knot, ok := r.Context().Value("knot").(string) 955 if !ok { 956 log.Println("malformed middleware") 957 return nil, fmt.Errorf("malformed middleware") 958 } 959 id, ok := r.Context().Value("resolvedId").(identity.Identity) 960 if !ok { 961 log.Println("malformed middleware") 962 return nil, fmt.Errorf("malformed middleware") 963 } 964 965 repoAt, ok := r.Context().Value("repoAt").(string) 966 if !ok { 967 log.Println("malformed middleware") 968 return nil, fmt.Errorf("malformed middleware") 969 } 970 971 return &FullyResolvedRepo{ 972 Knot: knot, 973 OwnerId: id, 974 RepoName: repoName, 975 RepoAt: repoAt, 976 }, nil 977} 978 979func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool { 980 settingsAllowed := false 981 if u != nil { 982 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo()) 983 if err == nil && ok { 984 settingsAllowed = true 985 } else { 986 log.Println(err, ok) 987 } 988 } 989 990 return settingsAllowed 991}