forked from tangled.org/core
this repo has no description
1package issues 2 3import ( 4 "fmt" 5 "log" 6 mathrand "math/rand/v2" 7 "net/http" 8 "slices" 9 "strconv" 10 "time" 11 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 "github.com/bluesky-social/indigo/atproto/data" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 "github.com/go-chi/chi/v5" 17 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/config" 20 "tangled.sh/tangled.sh/core/appview/db" 21 "tangled.sh/tangled.sh/core/appview/notify" 22 "tangled.sh/tangled.sh/core/appview/oauth" 23 "tangled.sh/tangled.sh/core/appview/pages" 24 "tangled.sh/tangled.sh/core/appview/pagination" 25 "tangled.sh/tangled.sh/core/appview/reporesolver" 26 "tangled.sh/tangled.sh/core/idresolver" 27 "tangled.sh/tangled.sh/core/tid" 28) 29 30type Issues struct { 31 oauth *oauth.OAuth 32 repoResolver *reporesolver.RepoResolver 33 pages *pages.Pages 34 idResolver *idresolver.Resolver 35 db *db.DB 36 config *config.Config 37 notifier notify.Notifier 38} 39 40func New( 41 oauth *oauth.OAuth, 42 repoResolver *reporesolver.RepoResolver, 43 pages *pages.Pages, 44 idResolver *idresolver.Resolver, 45 db *db.DB, 46 config *config.Config, 47 notifier notify.Notifier, 48) *Issues { 49 return &Issues{ 50 oauth: oauth, 51 repoResolver: repoResolver, 52 pages: pages, 53 idResolver: idResolver, 54 db: db, 55 config: config, 56 notifier: notifier, 57 } 58} 59 60func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 61 user := rp.oauth.GetUser(r) 62 f, err := rp.repoResolver.Resolve(r) 63 if err != nil { 64 log.Println("failed to get repo and knot", err) 65 return 66 } 67 68 issueId := chi.URLParam(r, "issue") 69 issueIdInt, err := strconv.Atoi(issueId) 70 if err != nil { 71 http.Error(w, "bad issue id", http.StatusBadRequest) 72 log.Println("failed to parse issue id", err) 73 return 74 } 75 76 issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) 77 if err != nil { 78 log.Println("failed to get issue and comments", err) 79 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 80 return 81 } 82 83 reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt)) 84 if err != nil { 85 log.Println("failed to get issue reactions") 86 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 87 } 88 89 userReactions := map[db.ReactionKind]bool{} 90 if user != nil { 91 userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt)) 92 } 93 94 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) 95 if err != nil { 96 log.Println("failed to resolve issue owner", err) 97 } 98 99 identsToResolve := make([]string, len(comments)) 100 for i, comment := range comments { 101 identsToResolve[i] = comment.OwnerDid 102 } 103 resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 104 didHandleMap := make(map[string]string) 105 for _, identity := range resolvedIds { 106 if !identity.Handle.IsInvalidHandle() { 107 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 108 } else { 109 didHandleMap[identity.DID.String()] = identity.DID.String() 110 } 111 } 112 113 rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 114 LoggedInUser: user, 115 RepoInfo: f.RepoInfo(user), 116 Issue: *issue, 117 Comments: comments, 118 119 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 120 DidHandleMap: didHandleMap, 121 122 OrderedReactionKinds: db.OrderedReactionKinds, 123 Reactions: reactionCountMap, 124 UserReacted: userReactions, 125 }) 126 127} 128 129func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { 130 user := rp.oauth.GetUser(r) 131 f, err := rp.repoResolver.Resolve(r) 132 if err != nil { 133 log.Println("failed to get repo and knot", err) 134 return 135 } 136 137 issueId := chi.URLParam(r, "issue") 138 issueIdInt, err := strconv.Atoi(issueId) 139 if err != nil { 140 http.Error(w, "bad issue id", http.StatusBadRequest) 141 log.Println("failed to parse issue id", err) 142 return 143 } 144 145 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 146 if err != nil { 147 log.Println("failed to get issue", err) 148 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 149 return 150 } 151 152 collaborators, err := f.Collaborators(r.Context()) 153 if err != nil { 154 log.Println("failed to fetch repo collaborators: %w", err) 155 } 156 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 157 return user.Did == collab.Did 158 }) 159 isIssueOwner := user.Did == issue.OwnerDid 160 161 // TODO: make this more granular 162 if isIssueOwner || isCollaborator { 163 164 closed := tangled.RepoIssueStateClosed 165 166 client, err := rp.oauth.AuthorizedClient(r) 167 if err != nil { 168 log.Println("failed to get authorized client", err) 169 return 170 } 171 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 172 Collection: tangled.RepoIssueStateNSID, 173 Repo: user.Did, 174 Rkey: tid.TID(), 175 Record: &lexutil.LexiconTypeDecoder{ 176 Val: &tangled.RepoIssueState{ 177 Issue: issue.IssueAt, 178 State: closed, 179 }, 180 }, 181 }) 182 183 if err != nil { 184 log.Println("failed to update issue state", err) 185 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 186 return 187 } 188 189 err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 190 if err != nil { 191 log.Println("failed to close issue", err) 192 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 193 return 194 } 195 196 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 197 return 198 } else { 199 log.Println("user is not permitted to close issue") 200 http.Error(w, "for biden", http.StatusUnauthorized) 201 return 202 } 203} 204 205func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { 206 user := rp.oauth.GetUser(r) 207 f, err := rp.repoResolver.Resolve(r) 208 if err != nil { 209 log.Println("failed to get repo and knot", err) 210 return 211 } 212 213 issueId := chi.URLParam(r, "issue") 214 issueIdInt, err := strconv.Atoi(issueId) 215 if err != nil { 216 http.Error(w, "bad issue id", http.StatusBadRequest) 217 log.Println("failed to parse issue id", err) 218 return 219 } 220 221 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 222 if err != nil { 223 log.Println("failed to get issue", err) 224 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 225 return 226 } 227 228 collaborators, err := f.Collaborators(r.Context()) 229 if err != nil { 230 log.Println("failed to fetch repo collaborators: %w", err) 231 } 232 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool { 233 return user.Did == collab.Did 234 }) 235 isIssueOwner := user.Did == issue.OwnerDid 236 237 if isCollaborator || isIssueOwner { 238 err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 239 if err != nil { 240 log.Println("failed to reopen issue", err) 241 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 242 return 243 } 244 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 245 return 246 } else { 247 log.Println("user is not the owner of the repo") 248 http.Error(w, "forbidden", http.StatusUnauthorized) 249 return 250 } 251} 252 253func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { 254 user := rp.oauth.GetUser(r) 255 f, err := rp.repoResolver.Resolve(r) 256 if err != nil { 257 log.Println("failed to get repo and knot", err) 258 return 259 } 260 261 issueId := chi.URLParam(r, "issue") 262 issueIdInt, err := strconv.Atoi(issueId) 263 if err != nil { 264 http.Error(w, "bad issue id", http.StatusBadRequest) 265 log.Println("failed to parse issue id", err) 266 return 267 } 268 269 switch r.Method { 270 case http.MethodPost: 271 body := r.FormValue("body") 272 if body == "" { 273 rp.pages.Notice(w, "issue", "Body is required") 274 return 275 } 276 277 commentId := mathrand.IntN(1000000) 278 rkey := tid.TID() 279 280 err := db.NewIssueComment(rp.db, &db.Comment{ 281 OwnerDid: user.Did, 282 RepoAt: f.RepoAt, 283 Issue: issueIdInt, 284 CommentId: commentId, 285 Body: body, 286 Rkey: rkey, 287 }) 288 if err != nil { 289 log.Println("failed to create comment", err) 290 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 291 return 292 } 293 294 createdAt := time.Now().Format(time.RFC3339) 295 commentIdInt64 := int64(commentId) 296 ownerDid := user.Did 297 issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 298 if err != nil { 299 log.Println("failed to get issue at", err) 300 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 301 return 302 } 303 304 atUri := f.RepoAt.String() 305 client, err := rp.oauth.AuthorizedClient(r) 306 if err != nil { 307 log.Println("failed to get authorized client", err) 308 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 309 return 310 } 311 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 312 Collection: tangled.RepoIssueCommentNSID, 313 Repo: user.Did, 314 Rkey: rkey, 315 Record: &lexutil.LexiconTypeDecoder{ 316 Val: &tangled.RepoIssueComment{ 317 Repo: &atUri, 318 Issue: issueAt, 319 CommentId: &commentIdInt64, 320 Owner: &ownerDid, 321 Body: body, 322 CreatedAt: createdAt, 323 }, 324 }, 325 }) 326 if err != nil { 327 log.Println("failed to create comment", err) 328 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 329 return 330 } 331 332 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 333 return 334 } 335} 336 337func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { 338 user := rp.oauth.GetUser(r) 339 f, err := rp.repoResolver.Resolve(r) 340 if err != nil { 341 log.Println("failed to get repo and knot", err) 342 return 343 } 344 345 issueId := chi.URLParam(r, "issue") 346 issueIdInt, err := strconv.Atoi(issueId) 347 if err != nil { 348 http.Error(w, "bad issue id", http.StatusBadRequest) 349 log.Println("failed to parse issue id", err) 350 return 351 } 352 353 commentId := chi.URLParam(r, "comment_id") 354 commentIdInt, err := strconv.Atoi(commentId) 355 if err != nil { 356 http.Error(w, "bad comment id", http.StatusBadRequest) 357 log.Println("failed to parse issue id", err) 358 return 359 } 360 361 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 362 if err != nil { 363 log.Println("failed to get issue", err) 364 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 365 return 366 } 367 368 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 369 if err != nil { 370 http.Error(w, "bad comment id", http.StatusBadRequest) 371 return 372 } 373 374 identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid) 375 if err != nil { 376 log.Println("failed to resolve did") 377 return 378 } 379 380 didHandleMap := make(map[string]string) 381 if !identity.Handle.IsInvalidHandle() { 382 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 383 } else { 384 didHandleMap[identity.DID.String()] = identity.DID.String() 385 } 386 387 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 388 LoggedInUser: user, 389 RepoInfo: f.RepoInfo(user), 390 DidHandleMap: didHandleMap, 391 Issue: issue, 392 Comment: comment, 393 }) 394} 395 396func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { 397 user := rp.oauth.GetUser(r) 398 f, err := rp.repoResolver.Resolve(r) 399 if err != nil { 400 log.Println("failed to get repo and knot", err) 401 return 402 } 403 404 issueId := chi.URLParam(r, "issue") 405 issueIdInt, err := strconv.Atoi(issueId) 406 if err != nil { 407 http.Error(w, "bad issue id", http.StatusBadRequest) 408 log.Println("failed to parse issue id", err) 409 return 410 } 411 412 commentId := chi.URLParam(r, "comment_id") 413 commentIdInt, err := strconv.Atoi(commentId) 414 if err != nil { 415 http.Error(w, "bad comment id", http.StatusBadRequest) 416 log.Println("failed to parse issue id", err) 417 return 418 } 419 420 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 421 if err != nil { 422 log.Println("failed to get issue", err) 423 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 424 return 425 } 426 427 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 428 if err != nil { 429 http.Error(w, "bad comment id", http.StatusBadRequest) 430 return 431 } 432 433 if comment.OwnerDid != user.Did { 434 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 435 return 436 } 437 438 switch r.Method { 439 case http.MethodGet: 440 rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 441 LoggedInUser: user, 442 RepoInfo: f.RepoInfo(user), 443 Issue: issue, 444 Comment: comment, 445 }) 446 case http.MethodPost: 447 // extract form value 448 newBody := r.FormValue("body") 449 client, err := rp.oauth.AuthorizedClient(r) 450 if err != nil { 451 log.Println("failed to get authorized client", err) 452 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 453 return 454 } 455 rkey := comment.Rkey 456 457 // optimistic update 458 edited := time.Now() 459 err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 460 if err != nil { 461 log.Println("failed to perferom update-description query", err) 462 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 463 return 464 } 465 466 // rkey is optional, it was introduced later 467 if comment.Rkey != "" { 468 // update the record on pds 469 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 470 if err != nil { 471 // failed to get record 472 log.Println(err, rkey) 473 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 474 return 475 } 476 value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json 477 record, _ := data.UnmarshalJSON(value) 478 479 repoAt := record["repo"].(string) 480 issueAt := record["issue"].(string) 481 createdAt := record["createdAt"].(string) 482 commentIdInt64 := int64(commentIdInt) 483 484 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 485 Collection: tangled.RepoIssueCommentNSID, 486 Repo: user.Did, 487 Rkey: rkey, 488 SwapRecord: ex.Cid, 489 Record: &lexutil.LexiconTypeDecoder{ 490 Val: &tangled.RepoIssueComment{ 491 Repo: &repoAt, 492 Issue: issueAt, 493 CommentId: &commentIdInt64, 494 Owner: &comment.OwnerDid, 495 Body: newBody, 496 CreatedAt: createdAt, 497 }, 498 }, 499 }) 500 if err != nil { 501 log.Println(err) 502 } 503 } 504 505 // optimistic update for htmx 506 didHandleMap := map[string]string{ 507 user.Did: user.Handle, 508 } 509 comment.Body = newBody 510 comment.Edited = &edited 511 512 // return new comment body with htmx 513 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 514 LoggedInUser: user, 515 RepoInfo: f.RepoInfo(user), 516 DidHandleMap: didHandleMap, 517 Issue: issue, 518 Comment: comment, 519 }) 520 return 521 522 } 523 524} 525 526func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 527 user := rp.oauth.GetUser(r) 528 f, err := rp.repoResolver.Resolve(r) 529 if err != nil { 530 log.Println("failed to get repo and knot", err) 531 return 532 } 533 534 issueId := chi.URLParam(r, "issue") 535 issueIdInt, err := strconv.Atoi(issueId) 536 if err != nil { 537 http.Error(w, "bad issue id", http.StatusBadRequest) 538 log.Println("failed to parse issue id", err) 539 return 540 } 541 542 issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 543 if err != nil { 544 log.Println("failed to get issue", err) 545 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 546 return 547 } 548 549 commentId := chi.URLParam(r, "comment_id") 550 commentIdInt, err := strconv.Atoi(commentId) 551 if err != nil { 552 http.Error(w, "bad comment id", http.StatusBadRequest) 553 log.Println("failed to parse issue id", err) 554 return 555 } 556 557 comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 558 if err != nil { 559 http.Error(w, "bad comment id", http.StatusBadRequest) 560 return 561 } 562 563 if comment.OwnerDid != user.Did { 564 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 565 return 566 } 567 568 if comment.Deleted != nil { 569 http.Error(w, "comment already deleted", http.StatusBadRequest) 570 return 571 } 572 573 // optimistic deletion 574 deleted := time.Now() 575 err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 576 if err != nil { 577 log.Println("failed to delete comment") 578 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 579 return 580 } 581 582 // delete from pds 583 if comment.Rkey != "" { 584 client, err := rp.oauth.AuthorizedClient(r) 585 if err != nil { 586 log.Println("failed to get authorized client", err) 587 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 588 return 589 } 590 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 591 Collection: tangled.GraphFollowNSID, 592 Repo: user.Did, 593 Rkey: comment.Rkey, 594 }) 595 if err != nil { 596 log.Println(err) 597 } 598 } 599 600 // optimistic update for htmx 601 didHandleMap := map[string]string{ 602 user.Did: user.Handle, 603 } 604 comment.Body = "" 605 comment.Deleted = &deleted 606 607 // htmx fragment of comment after deletion 608 rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 609 LoggedInUser: user, 610 RepoInfo: f.RepoInfo(user), 611 DidHandleMap: didHandleMap, 612 Issue: issue, 613 Comment: comment, 614 }) 615 return 616} 617 618func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { 619 params := r.URL.Query() 620 state := params.Get("state") 621 isOpen := true 622 switch state { 623 case "open": 624 isOpen = true 625 case "closed": 626 isOpen = false 627 default: 628 isOpen = true 629 } 630 631 page, ok := r.Context().Value("page").(pagination.Page) 632 if !ok { 633 log.Println("failed to get page") 634 page = pagination.FirstPage() 635 } 636 637 user := rp.oauth.GetUser(r) 638 f, err := rp.repoResolver.Resolve(r) 639 if err != nil { 640 log.Println("failed to get repo and knot", err) 641 return 642 } 643 644 issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 645 if err != nil { 646 log.Println("failed to get issues", err) 647 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 648 return 649 } 650 651 identsToResolve := make([]string, len(issues)) 652 for i, issue := range issues { 653 identsToResolve[i] = issue.OwnerDid 654 } 655 resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 656 didHandleMap := make(map[string]string) 657 for _, identity := range resolvedIds { 658 if !identity.Handle.IsInvalidHandle() { 659 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 660 } else { 661 didHandleMap[identity.DID.String()] = identity.DID.String() 662 } 663 } 664 665 rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 666 LoggedInUser: rp.oauth.GetUser(r), 667 RepoInfo: f.RepoInfo(user), 668 Issues: issues, 669 DidHandleMap: didHandleMap, 670 FilteringByOpen: isOpen, 671 Page: page, 672 }) 673 return 674} 675 676func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { 677 user := rp.oauth.GetUser(r) 678 679 f, err := rp.repoResolver.Resolve(r) 680 if err != nil { 681 log.Println("failed to get repo and knot", err) 682 return 683 } 684 685 switch r.Method { 686 case http.MethodGet: 687 rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 688 LoggedInUser: user, 689 RepoInfo: f.RepoInfo(user), 690 }) 691 case http.MethodPost: 692 title := r.FormValue("title") 693 body := r.FormValue("body") 694 695 if title == "" || body == "" { 696 rp.pages.Notice(w, "issues", "Title and body are required") 697 return 698 } 699 700 tx, err := rp.db.BeginTx(r.Context(), nil) 701 if err != nil { 702 rp.pages.Notice(w, "issues", "Failed to create issue, try again later") 703 return 704 } 705 706 issue := &db.Issue{ 707 RepoAt: f.RepoAt, 708 Title: title, 709 Body: body, 710 OwnerDid: user.Did, 711 } 712 err = db.NewIssue(tx, issue) 713 if err != nil { 714 log.Println("failed to create issue", err) 715 rp.pages.Notice(w, "issues", "Failed to create issue.") 716 return 717 } 718 719 client, err := rp.oauth.AuthorizedClient(r) 720 if err != nil { 721 log.Println("failed to get authorized client", err) 722 rp.pages.Notice(w, "issues", "Failed to create issue.") 723 return 724 } 725 atUri := f.RepoAt.String() 726 resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 727 Collection: tangled.RepoIssueNSID, 728 Repo: user.Did, 729 Rkey: tid.TID(), 730 Record: &lexutil.LexiconTypeDecoder{ 731 Val: &tangled.RepoIssue{ 732 Repo: atUri, 733 Title: title, 734 Body: &body, 735 Owner: user.Did, 736 IssueId: int64(issue.IssueId), 737 }, 738 }, 739 }) 740 if err != nil { 741 log.Println("failed to create issue", err) 742 rp.pages.Notice(w, "issues", "Failed to create issue.") 743 return 744 } 745 746 err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) 747 if err != nil { 748 log.Println("failed to set issue at", err) 749 rp.pages.Notice(w, "issues", "Failed to create issue.") 750 return 751 } 752 753 rp.notifier.NewIssue(r.Context(), issue) 754 755 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 756 return 757 } 758}