···
9
+
"github.com/bluesky-social/indigo/api/atproto"
10
+
"github.com/bluesky-social/indigo/atproto/syntax"
11
+
lexutil "github.com/bluesky-social/indigo/lex/util"
12
+
"tangled.org/core/api/tangled"
13
+
"tangled.org/core/appview/config"
14
+
"tangled.org/core/appview/db"
15
+
issues_indexer "tangled.org/core/appview/indexer/issues"
16
+
"tangled.org/core/appview/models"
17
+
"tangled.org/core/appview/notify"
18
+
"tangled.org/core/appview/pages/markup"
19
+
"tangled.org/core/appview/session"
20
+
"tangled.org/core/appview/validator"
21
+
"tangled.org/core/idresolver"
22
+
"tangled.org/core/rbac"
23
+
"tangled.org/core/tid"
26
+
type Service struct {
27
+
config *config.Config
29
+
enforcer *rbac.Enforcer
30
+
indexer *issues_indexer.Indexer
32
+
notifier notify.Notifier
33
+
idResolver *idresolver.Resolver
34
+
validator *validator.Validator
38
+
logger *slog.Logger,
39
+
config *config.Config,
41
+
enforcer *rbac.Enforcer,
42
+
notifier notify.Notifier,
43
+
idResolver *idresolver.Resolver,
44
+
indexer *issues_indexer.Indexer,
45
+
validator *validator.Validator,
60
+
ErrUnAuthenticated = errors.New("user session missing")
61
+
ErrForbidden = errors.New("unauthorized operation")
62
+
ErrDatabaseFail = errors.New("db op fail")
63
+
ErrPDSFail = errors.New("pds op fail")
64
+
ErrValidationFail = errors.New("issue validation fail")
67
+
func (s *Service) NewIssue(ctx context.Context, repo *models.Repo, title, body string) (*models.Issue, error) {
68
+
l := s.logger.With("method", "NewIssue")
69
+
sess := session.FromContext(ctx)
71
+
l.Error("user session is missing in context")
72
+
return nil, ErrForbidden
74
+
authorDid := sess.Data.AccountDID
75
+
l = l.With("did", authorDid)
77
+
// mentions, references := s.refResolver.Resolve(ctx, body)
78
+
mentions := func() []syntax.DID {
79
+
rawMentions := markup.FindUserMentions(body)
80
+
idents := s.idResolver.ResolveIdents(ctx, rawMentions)
81
+
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
82
+
var mentions []syntax.DID
83
+
for _, ident := range idents {
84
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
85
+
mentions = append(mentions, ident.DID)
91
+
issue := models.Issue{
92
+
RepoAt: repo.RepoAt(),
97
+
Did: authorDid.String(),
98
+
Created: time.Now(),
102
+
if err := s.validator.ValidateIssue(&issue); err != nil {
103
+
l.Error("validation error", "err", err)
104
+
return nil, ErrValidationFail
107
+
tx, err := s.db.BeginTx(ctx, nil)
109
+
l.Error("db.BeginTx failed", "err", err)
110
+
return nil, ErrDatabaseFail
112
+
defer tx.Rollback()
114
+
if err := db.PutIssue(tx, &issue); err != nil {
115
+
l.Error("db.PutIssue failed", "err", err)
116
+
return nil, ErrDatabaseFail
119
+
atpclient := sess.APIClient()
120
+
record := issue.AsRecord()
121
+
_, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{
122
+
Repo: authorDid.String(),
123
+
Collection: tangled.RepoIssueNSID,
125
+
Record: &lexutil.LexiconTypeDecoder{
130
+
l.Error("atproto.RepoPutRecord failed", "err", err)
131
+
return nil, ErrPDSFail
133
+
if err = tx.Commit(); err != nil {
134
+
l.Error("tx.Commit failed", "err", err)
135
+
return nil, ErrDatabaseFail
138
+
s.notifier.NewIssue(ctx, &issue, mentions)
142
+
func (s *Service) GetIssues(ctx context.Context, repo *models.Repo, searchOpts models.IssueSearchOptions) ([]models.Issue, error) {
143
+
l := s.logger.With("method", "EditIssue")
145
+
var issues []models.Issue
147
+
if searchOpts.Keyword != "" {
148
+
res, err := s.indexer.Search(ctx, searchOpts)
150
+
l.Error("failed to search for issues", "err", err)
153
+
l.Debug("searched issues with indexer", "count", len(res.Hits))
154
+
issues, err = db.GetIssues(s.db, db.FilterIn("id", res.Hits))
156
+
l.Error("failed to get issues", "err", err)
161
+
if searchOpts.IsOpen {
164
+
issues, err = db.GetIssuesPaginated(
167
+
db.FilterEq("repo_at", repo.RepoAt()),
168
+
db.FilterEq("open", openInt),
171
+
l.Error("failed to get issues", "err", err)
179
+
func (s *Service) EditIssue(ctx context.Context, issue *models.Issue) error {
180
+
l := s.logger.With("method", "EditIssue")
181
+
sess := session.FromContext(ctx)
183
+
l.Error("user session is missing in context")
184
+
return ErrForbidden
186
+
sessDid := sess.Data.AccountDID
187
+
l = l.With("did", sessDid)
189
+
if sessDid != syntax.DID(issue.Did) {
190
+
l.Error("only author can edit the issue")
191
+
return ErrForbidden
194
+
if err := s.validator.ValidateIssue(issue); err != nil {
195
+
l.Error("validation error", "err", err)
196
+
return ErrValidationFail
199
+
tx, err := s.db.BeginTx(ctx, nil)
201
+
l.Error("db.BeginTx failed", "err", err)
202
+
return ErrDatabaseFail
204
+
defer tx.Rollback()
206
+
if err := db.PutIssue(tx, issue); err != nil {
207
+
l.Error("db.PutIssue failed", "err", err)
208
+
return ErrDatabaseFail
211
+
atpclient := sess.APIClient()
212
+
record := issue.AsRecord()
214
+
ex, err := atproto.RepoGetRecord(ctx, atpclient, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey)
216
+
l.Error("atproto.RepoGetRecord failed", "err", err)
219
+
_, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{
220
+
Collection: tangled.RepoIssueNSID,
221
+
SwapRecord: ex.Cid,
222
+
Record: &lexutil.LexiconTypeDecoder{
227
+
l.Error("atproto.RepoPutRecord failed", "err", err)
231
+
if err = tx.Commit(); err != nil {
232
+
l.Error("tx.Commit failed", "err", err)
233
+
return ErrDatabaseFail
236
+
// TODO: notify PutIssue
241
+
func (s *Service) DeleteIssue(ctx context.Context, issue *models.Issue) error {
242
+
l := s.logger.With("method", "DeleteIssue")
243
+
sess := session.FromContext(ctx)
245
+
l.Error("user session is missing in context")
246
+
return ErrForbidden
248
+
sessDid := sess.Data.AccountDID
249
+
l = l.With("did", sessDid)
251
+
if sessDid != syntax.DID(issue.Did) {
252
+
l.Error("only author can edit the issue")
253
+
return ErrForbidden
256
+
tx, err := s.db.BeginTx(ctx, nil)
258
+
l.Error("db.BeginTx failed", "err", err)
259
+
return ErrDatabaseFail
261
+
defer tx.Rollback()
263
+
if err := db.DeleteIssues(tx, db.FilterEq("id", issue.Id)); err != nil {
264
+
l.Error("db.DeleteIssues failed", "err", err)
265
+
return ErrDatabaseFail
268
+
atpclient := sess.APIClient()
269
+
_, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{
270
+
Collection: tangled.RepoIssueNSID,
275
+
l.Error("atproto.RepoDeleteRecord failed", "err", err)
279
+
if err := tx.Commit(); err != nil {
280
+
l.Error("tx.Commit failed", "err", err)
281
+
return ErrDatabaseFail
284
+
s.notifier.DeleteIssue(ctx, issue)