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