Monorepo for Tangled
tangled.org
1package pull
2
3import (
4 "context"
5 "log/slog"
6 "time"
7
8 "github.com/bluesky-social/indigo/api/atproto"
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 lexutil "github.com/bluesky-social/indigo/lex/util"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/config"
13 "tangled.org/core/appview/db"
14 pulls_indexer "tangled.org/core/appview/indexer/pulls"
15 "tangled.org/core/appview/mentions"
16 "tangled.org/core/appview/models"
17 "tangled.org/core/appview/notify"
18 "tangled.org/core/appview/session"
19 "tangled.org/core/idresolver"
20 "tangled.org/core/rbac"
21 "tangled.org/core/tid"
22)
23
24type Service struct {
25 config *config.Config
26 db *db.DB
27 enforcer *rbac.Enforcer
28 indexer *pulls_indexer.Indexer
29 logger *slog.Logger
30 notifier notify.Notifier
31 idResolver *idresolver.Resolver
32 mentionsResolver *mentions.Resolver
33}
34
35func NewService(
36 logger *slog.Logger,
37 config *config.Config,
38 db *db.DB,
39 enforcer *rbac.Enforcer,
40 notifier notify.Notifier,
41 idResolver *idresolver.Resolver,
42 mentionsResolver *mentions.Resolver,
43 indexer *pulls_indexer.Indexer,
44) Service {
45 return Service{
46 config,
47 db,
48 enforcer,
49 indexer,
50 logger,
51 notifier,
52 idResolver,
53 mentionsResolver,
54 }
55}
56
57// SubmitPull creates a new PR or resubmits existing PR.
58// `pull` can be `nil` for creating a new PR.
59func (s *Service) SubmitPull(
60 ctx context.Context,
61 pull *models.Pull2,
62 target models.PullTarget,
63 source *models.PullSource2,
64 patch, title, body string,
65) error {
66 l := s.logger.With("method", "NewPullSubmission")
67 sess := session.FromContext(ctx)
68 if sess == nil {
69 l.Error("user session is missing in context")
70 return ErrForbidden
71 }
72 sessDid := sess.Data.AccountDID
73 l = l.With("did", sessDid)
74
75 var (
76 did syntax.DID
77 rkey syntax.RecordKey
78 )
79 if pull == nil {
80 // new pr
81 did = sessDid
82 rkey = syntax.RecordKey(tid.TID())
83 } else {
84 // resubmit
85 if sessDid != pull.Did {
86 l.Error("only author can edit the pull")
87 return ErrForbidden
88 }
89 did = pull.Did
90 rkey = pull.Rkey
91 }
92
93 mentions, references := s.mentionsResolver.Resolve(ctx, body)
94
95 round := models.PullRound{
96 Did: did,
97 Rkey: rkey,
98 Target: target,
99 Source: source,
100 Patch: patch,
101 Title: title,
102 Body: body,
103 Mentions: mentions,
104 References: references,
105 Created: time.Now(),
106 }
107 if err := round.Validate(); err != nil {
108 l.Error("validation error", "err", err)
109 return ErrValidationFail
110 }
111
112 atpclient := sess.APIClient()
113 record := round.AsRecord()
114
115 var exCid *string
116 if pull != nil {
117 x := pull.CID().String()
118 exCid = &x
119 }
120 resp, err := atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{
121 Collection: tangled.RepoPullNSID,
122 SwapRecord: exCid,
123 Record: &lexutil.LexiconTypeDecoder{
124 Val: &record,
125 },
126 })
127 if err != nil {
128 l.Error("atproto.RepoPutRecord failed", "err", err)
129 return ErrPDSFail
130 }
131 round.Cid = syntax.CID(resp.Cid)
132
133 tx, err := s.db.BeginTx(ctx, nil)
134 if err != nil {
135 l.Error("db.BeginTx failed", "err", err)
136 return ErrDatabaseFail
137 }
138 defer tx.Rollback()
139
140 if err := db.NewPullRound(tx, 0, &round); err != nil {
141 l.Error("db.UpdatePull2 failed", "err", err)
142 return ErrDatabaseFail
143 }
144
145 if err = tx.Commit(); err != nil {
146 l.Error("tx.Commit failed", "err", err)
147 return ErrDatabaseFail
148 }
149
150 if pull == nil {
151 // s.notifier.NewPull(ctx, &round)
152 } else {
153 pull.Submissions = append(pull.Submissions, &round)
154 // s.notifier.ResubmitPull(ctx, &round)
155 }
156
157 return nil
158}
159
160func (s *Service) DeletePull(ctx context.Context, pull *models.Pull2) error {
161 l := s.logger.With("method", "DeletePull")
162 sess := session.FromContext(ctx)
163 if sess == nil {
164 l.Error("user session is missing in context")
165 return ErrForbidden
166 }
167 sessDid := sess.Data.AccountDID
168 l = l.With("did", sessDid)
169
170 if sessDid != pull.Did {
171 l.Error("only author can delete the pull")
172 return ErrForbidden
173 }
174
175 tx, err := s.db.BeginTx(ctx, nil)
176 if err != nil {
177 l.Error("db.BeginTx failed", "err", err)
178 return ErrDatabaseFail
179 }
180 defer tx.Rollback()
181
182 if err := db.DeletePull2(tx, pull.AtUri()); err != nil {
183 l.Error("db.DeletePull2 failed", "err", err)
184 return ErrDatabaseFail
185 }
186
187 atpclient := sess.APIClient()
188 _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{
189 Collection: tangled.RepoIssueNSID,
190 Repo: pull.Did.String(),
191 Rkey: pull.Rkey.String(),
192 })
193 if err != nil {
194 l.Error("atproto.RepoDeleteRecord failed", "err", err)
195 return ErrPDSFail
196 }
197
198 if err := tx.Commit(); err != nil {
199 l.Error("tx.Commit failed", "err", err)
200 return ErrDatabaseFail
201 }
202
203 pull.State = models.PullDeleted
204
205 // s.notifier.DeletePull(ctx, pull)
206 return nil
207}
208
209func (s *Service) ListPulls(ctx context.Context, repo *models.Repo, searchOpts models.PullSearchOptions) ([]*models.Pull2, error) {
210 l := s.logger.With("method", "ListPulls")
211
212 var pulls []*models.Pull2
213 var err error
214 if searchOpts.Keyword != "" {
215 res, err := s.indexer.Search(ctx, searchOpts)
216 if err != nil {
217 l.Error("failed to search for pulls", "err", err)
218 return nil, ErrIndexerFail
219 }
220 l.Debug("searched pulls with indexer", "count", len(res.Hits))
221 pulls, err = db.GetPulls2(s.db, db.FilterIn("id", res.Hits))
222 if err != nil {
223 l.Error("failed to get pulls", "err", err)
224 return nil, ErrDatabaseFail
225 }
226 } else {
227 pulls, err = db.GetPullsPaginated(
228 s.db,
229 searchOpts.Page,
230 db.FilterEq("repo_at", repo.RepoAt()),
231 db.FilterEq("state", searchOpts.State),
232 )
233 if err != nil {
234 l.Error("failed to get pulls", "err", err)
235 return nil, ErrDatabaseFail
236 }
237 }
238 return pulls, nil
239}