Monorepo for Tangled
tangled.org
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}