Monorepo for Tangled tangled.org
10

Configure Feed

Select the types of activity you want to include in your feed.

1package pulls 2 3import ( 4 "fmt" 5 "net/http" 6 "strconv" 7 "time" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/db" 11 "tangled.org/core/appview/models" 12 "tangled.org/core/appview/pages" 13 "tangled.org/core/appview/reporesolver" 14 "tangled.org/core/tid" 15 16 comatproto "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 20 "github.com/go-chi/chi/v5" 21) 22 23func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 24 l := s.logger.With("handler", "PullComment") 25 26 user := s.oauth.GetMultiAccountUser(r) 27 if user != nil { 28 l = l.With("user", user.Did) 29 } 30 31 f, err := s.repoResolver.Resolve(r) 32 if err != nil { 33 l.Error("failed to get repo and knot", "err", err) 34 return 35 } 36 37 pull, ok := r.Context().Value("pull").(*models.Pull) 38 if !ok { 39 l.Error("failed to get pull") 40 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 41 return 42 } 43 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid) 44 45 roundNumberStr := chi.URLParam(r, "round") 46 roundNumber, err := strconv.Atoi(roundNumberStr) 47 if err != nil || roundNumber >= len(pull.Submissions) { 48 http.Error(w, "bad round id", http.StatusBadRequest) 49 l.Error("failed to parse round id", "err", err, "round_number_str", roundNumberStr) 50 return 51 } 52 53 switch r.Method { 54 case http.MethodGet: 55 s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{ 56 LoggedInUser: user, 57 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 58 Pull: pull, 59 RoundNumber: roundNumber, 60 }) 61 return 62 case http.MethodPost: 63 body := r.FormValue("body") 64 if body == "" { 65 s.pages.Notice(w, "pull-comment", "Comment body is required") 66 return 67 } 68 69 // TODO(boltless): normalize markdown body 70 normalizedBody := body 71 mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 72 73 markdownBody := tangled.MarkupMarkdown{ 74 Text: normalizedBody, 75 Original: &body, 76 Blobs: nil, 77 } 78 79 // ingest CID of PR record on-demand. 80 // TODO(boltless): appview should ingest CID of atproto records 81 cid, err := func() (syntax.CID, error) { 82 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 83 if err != nil { 84 return "", err 85 } 86 87 xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} 88 out, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoPullNSID, pull.OwnerDid, pull.Rkey) 89 if err != nil { 90 return "", err 91 } 92 if out.Cid == nil { 93 return "", fmt.Errorf("record CID is empty") 94 } 95 96 cid, err := syntax.ParseCID(*out.Cid) 97 if err != nil { 98 return "", err 99 } 100 101 return cid, nil 102 }() 103 if err != nil { 104 s.logger.Error("failed to backfill subject PR record", "err", err) 105 s.pages.Notice(w, "pull-comment", "failed to backfill subject record") 106 return 107 } 108 pullStrongRef := comatproto.RepoStrongRef{ 109 Uri: pull.AtUri().String(), 110 Cid: cid.String(), 111 } 112 113 comment := models.Comment{ 114 Did: syntax.DID(user.Did), 115 Collection: tangled.FeedCommentNSID, 116 Rkey: syntax.RecordKey(tid.TID()), 117 118 Subject: pullStrongRef, 119 Body: markdownBody, 120 Created: time.Now(), 121 ReplyTo: nil, 122 PullRoundIdx: &roundNumber, 123 } 124 if err = comment.Validate(); err != nil { 125 s.logger.Error("failed to validate comment", "err", err) 126 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 127 return 128 } 129 130 client, err := s.oauth.AuthorizedClient(r) 131 if err != nil { 132 s.logger.Error("failed to get authorized client", "err", err) 133 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 134 return 135 } 136 137 out, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 138 Collection: comment.Collection.String(), 139 Repo: comment.Did.String(), 140 Rkey: comment.Rkey.String(), 141 Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, 142 }) 143 if err != nil { 144 s.logger.Error("failed to create pull comment", "err", err) 145 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 146 return 147 } 148 149 comment.Cid = syntax.CID(out.Cid) 150 151 // Start a transaction 152 tx, err := s.db.BeginTx(r.Context(), nil) 153 if err != nil { 154 l.Error("failed to start transaction", "err", err) 155 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 156 return 157 } 158 defer tx.Rollback() 159 160 // Create the pull comment in the database 161 err = db.PutComment(tx, &comment, references) 162 if err != nil { 163 l.Error("failed to create pull comment in database", "err", err) 164 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 165 return 166 } 167 168 // Commit the transaction 169 if err = tx.Commit(); err != nil { 170 l.Error("failed to commit transaction", "err", err) 171 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 172 return 173 } 174 175 s.notifier.NewPullComment(r.Context(), &comment, mentions) 176 177 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 178 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 179 return 180 } 181}