this repo has no description
1package issue
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 issues_indexer "tangled.org/core/appview/indexer/issues"
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/appview/validator"
20 "tangled.org/core/idresolver"
21 "tangled.org/core/orm"
22 "tangled.org/core/rbac"
23 "tangled.org/core/tid"
24)
25
26type Service struct {
27 config *config.Config
28 db *db.DB
29 enforcer *rbac.Enforcer
30 indexer *issues_indexer.Indexer
31 logger *slog.Logger
32 notifier notify.Notifier
33 idResolver *idresolver.Resolver
34 refResolver *mentions.Resolver
35 validator *validator.Validator
36}
37
38func NewService(
39 logger *slog.Logger,
40 config *config.Config,
41 db *db.DB,
42 enforcer *rbac.Enforcer,
43 notifier notify.Notifier,
44 idResolver *idresolver.Resolver,
45 refResolver *mentions.Resolver,
46 indexer *issues_indexer.Indexer,
47 validator *validator.Validator,
48) Service {
49 return Service{
50 config,
51 db,
52 enforcer,
53 indexer,
54 logger,
55 notifier,
56 idResolver,
57 refResolver,
58 validator,
59 }
60}
61
62func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) {
63 l := s.logger.With("method", "NewIssue")
64 sess := session.FromContext(ctx)
65 if sess == nil {
66 l.Error("user session is missing in context")
67 return nil, ErrForbidden
68 }
69 authorDid := sess.Data.AccountDID
70 l = l.With("did", authorDid)
71
72 mentions, references := s.refResolver.Resolve(ctx, body)
73
74 issue := models.Issue{
75 RepoAt: repo.RepoAt(),
76 Rkey: tid.TID(),
77 Title: title,
78 Body: body,
79 Open: true,
80 Did: authorDid.String(),
81 Created: time.Now(),
82 Mentions: mentions,
83 References: references,
84 Repo: repo,
85 }
86
87 if err := s.validator.ValidateIssue(&issue); err != nil {
88 l.Error("validation error", "err", err)
89 return nil, ErrValidationFail
90 }
91
92 tx, err := s.db.BeginTx(ctx, nil)
93 if err != nil {
94 l.Error("db.BeginTx failed", "err", err)
95 return nil, ErrDatabaseFail
96 }
97 defer tx.Rollback()
98
99 if err := db.PutIssue(tx, &issue); err != nil {
100 l.Error("db.PutIssue failed", "err", err)
101 return nil, ErrDatabaseFail
102 }
103
104 atpclient := sess.APIClient()
105 record := issue.AsRecord()
106 _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{
107 Repo: authorDid.String(),
108 Collection: tangled.RepoIssueNSID,
109 Rkey: issue.Rkey,
110 Record: &lexutil.LexiconTypeDecoder{
111 Val: &record,
112 },
113 })
114 if err != nil {
115 l.Error("atproto.RepoPutRecord failed", "err", err)
116 return nil, ErrPDSFail
117 }
118 if err = tx.Commit(); err != nil {
119 l.Error("tx.Commit failed", "err", err)
120 return nil, ErrDatabaseFail
121 }
122
123 s.notifier.NewIssue(ctx, &issue, mentions)
124 return &issue, nil
125}
126
127func (s *Service) GetIssues(ctx context.Context, repo *models.Repo, searchOpts models.IssueSearchOptions) ([]models.Issue, error) {
128 l := s.logger.With("method", "GetIssues")
129
130 var issues []models.Issue
131 var err error
132 if searchOpts.Keyword != "" {
133 res, err := s.indexer.Search(ctx, searchOpts)
134 if err != nil {
135 l.Error("failed to search for issues", "err", err)
136 return nil, ErrIndexerFail
137 }
138 l.Debug("searched issues with indexer", "count", len(res.Hits))
139 issues, err = db.GetIssues(s.db, orm.FilterIn("id", res.Hits))
140 if err != nil {
141 l.Error("failed to get issues", "err", err)
142 return nil, ErrDatabaseFail
143 }
144 } else {
145 openInt := 0
146 if searchOpts.IsOpen {
147 openInt = 1
148 }
149 issues, err = db.GetIssuesPaginated(
150 s.db,
151 searchOpts.Page,
152 orm.FilterEq("repo_at", repo.RepoAt()),
153 orm.FilterEq("open", openInt),
154 )
155 if err != nil {
156 l.Error("failed to get issues", "err", err)
157 return nil, ErrDatabaseFail
158 }
159 }
160
161 return issues, nil
162}
163
164func (s *Service) EditIssue(ctx context.Context, issue *models.Issue) error {
165 l := s.logger.With("method", "EditIssue")
166 sess := session.FromContext(ctx)
167 if sess == nil {
168 l.Error("user session is missing in context")
169 return ErrForbidden
170 }
171 sessDid := sess.Data.AccountDID
172 l = l.With("did", sessDid)
173
174 mentions, references := s.refResolver.Resolve(ctx, issue.Body)
175 issue.Mentions = mentions
176 issue.References = references
177
178 if sessDid != syntax.DID(issue.Did) {
179 l.Error("only author can edit the issue")
180 return ErrForbidden
181 }
182
183 if err := s.validator.ValidateIssue(issue); err != nil {
184 l.Error("validation error", "err", err)
185 return ErrValidationFail
186 }
187
188 tx, err := s.db.BeginTx(ctx, nil)
189 if err != nil {
190 l.Error("db.BeginTx failed", "err", err)
191 return ErrDatabaseFail
192 }
193 defer tx.Rollback()
194
195 if err := db.PutIssue(tx, issue); err != nil {
196 l.Error("db.PutIssue failed", "err", err)
197 return ErrDatabaseFail
198 }
199
200 atpclient := sess.APIClient()
201 record := issue.AsRecord()
202
203 ex, err := atproto.RepoGetRecord(ctx, atpclient, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey)
204 if err != nil {
205 l.Error("atproto.RepoGetRecord failed", "err", err)
206 return ErrPDSFail
207 }
208 _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{
209 Collection: tangled.RepoIssueNSID,
210 SwapRecord: ex.Cid,
211 Record: &lexutil.LexiconTypeDecoder{
212 Val: &record,
213 },
214 })
215 if err != nil {
216 l.Error("atproto.RepoPutRecord failed", "err", err)
217 return ErrPDSFail
218 }
219
220 if err = tx.Commit(); err != nil {
221 l.Error("tx.Commit failed", "err", err)
222 return ErrDatabaseFail
223 }
224
225 // TODO: notify PutIssue
226
227 return nil
228}
229
230func (s *Service) DeleteIssue(ctx context.Context, issue *models.Issue) error {
231 l := s.logger.With("method", "DeleteIssue")
232 sess := session.FromContext(ctx)
233 if sess == nil {
234 l.Error("user session is missing in context")
235 return ErrForbidden
236 }
237 sessDid := sess.Data.AccountDID
238 l = l.With("did", sessDid)
239
240 if sessDid != syntax.DID(issue.Did) {
241 l.Error("only author can edit the issue")
242 return ErrForbidden
243 }
244
245 tx, err := s.db.BeginTx(ctx, nil)
246 if err != nil {
247 l.Error("db.BeginTx failed", "err", err)
248 return ErrDatabaseFail
249 }
250 defer tx.Rollback()
251
252 if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil {
253 l.Error("db.DeleteIssues failed", "err", err)
254 return ErrDatabaseFail
255 }
256
257 atpclient := sess.APIClient()
258 _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{
259 Collection: tangled.RepoIssueNSID,
260 Repo: issue.Did,
261 Rkey: issue.Rkey,
262 })
263 if err != nil {
264 l.Error("atproto.RepoDeleteRecord failed", "err", err)
265 return ErrPDSFail
266 }
267
268 if err := tx.Commit(); err != nil {
269 l.Error("tx.Commit failed", "err", err)
270 return ErrDatabaseFail
271 }
272
273 s.notifier.DeleteIssue(ctx, issue)
274 return nil
275}