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}