A vibe coded tangled fork which supports pijul.
at sl/uvpzuszrulvq 280 lines 6.9 kB view raw
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, ok := session.FromContext(ctx) 65 if !ok { 66 l.Error("user session is missing in context") 67 return nil, ErrForbidden 68 } 69 authorDid := syntax.DID(sess.User.Did) 70 atpclient := sess.AtpClient 71 l = l.With("did", authorDid) 72 73 mentions, references := s.refResolver.Resolve(ctx, body) 74 75 issue := models.Issue{ 76 Did: authorDid.String(), 77 Rkey: tid.TID(), 78 RepoAt: repo.RepoAt(), 79 Title: title, 80 Body: body, 81 Created: time.Now(), 82 Mentions: mentions, 83 References: references, 84 Open: true, 85 Repo: repo, 86 } 87 88 if err := s.validator.ValidateIssue(&issue); err != nil { 89 l.Error("validation error", "err", err) 90 return nil, ErrValidationFail 91 } 92 93 tx, err := s.db.BeginTx(ctx, nil) 94 if err != nil { 95 l.Error("db.BeginTx failed", "err", err) 96 return nil, ErrDatabaseFail 97 } 98 defer tx.Rollback() 99 100 if err := db.PutIssue(tx, &issue); err != nil { 101 l.Error("db.PutIssue failed", "err", err) 102 return nil, ErrDatabaseFail 103 } 104 105 record := issue.AsRecord() 106 _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 107 Repo: issue.Did, 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.HasSearchFilters() { 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 filters := []orm.Filter{ 146 orm.FilterEq("repo_at", repo.RepoAt()), 147 } 148 if searchOpts.IsOpen != nil { 149 openInt := 0 150 if *searchOpts.IsOpen { 151 openInt = 1 152 } 153 filters = append(filters, orm.FilterEq("open", openInt)) 154 } 155 issues, err = db.GetIssuesPaginated( 156 s.db, 157 searchOpts.Page, 158 filters..., 159 ) 160 if err != nil { 161 l.Error("failed to get issues", "err", err) 162 return nil, ErrDatabaseFail 163 } 164 } 165 166 return issues, nil 167} 168 169func (s *Service) EditIssue(ctx context.Context, issue *models.Issue) error { 170 l := s.logger.With("method", "EditIssue") 171 sess, ok := session.FromContext(ctx) 172 if !ok { 173 l.Error("user session is missing in context") 174 return ErrForbidden 175 } 176 atpclient := sess.AtpClient 177 l = l.With("did", sess.User.Did) 178 179 mentions, references := s.refResolver.Resolve(ctx, issue.Body) 180 issue.Mentions = mentions 181 issue.References = references 182 183 if sess.User.Did != issue.Did { 184 l.Error("only author can edit the issue") 185 return ErrForbidden 186 } 187 188 if err := s.validator.ValidateIssue(issue); err != nil { 189 l.Error("validation error", "err", err) 190 return ErrValidationFail 191 } 192 193 tx, err := s.db.BeginTx(ctx, nil) 194 if err != nil { 195 l.Error("db.BeginTx failed", "err", err) 196 return ErrDatabaseFail 197 } 198 defer tx.Rollback() 199 200 if err := db.PutIssue(tx, issue); err != nil { 201 l.Error("db.PutIssue failed", "err", err) 202 return ErrDatabaseFail 203 } 204 205 record := issue.AsRecord() 206 207 ex, err := atproto.RepoGetRecord(ctx, atpclient, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey) 208 if err != nil { 209 l.Error("atproto.RepoGetRecord failed", "err", err) 210 return ErrPDSFail 211 } 212 _, err = atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 213 Repo: issue.Did, 214 Collection: tangled.RepoIssueNSID, 215 Rkey: issue.Rkey, 216 SwapRecord: ex.Cid, 217 Record: &lexutil.LexiconTypeDecoder{ 218 Val: &record, 219 }, 220 }) 221 if err != nil { 222 l.Error("atproto.RepoPutRecord failed", "err", err) 223 return ErrPDSFail 224 } 225 226 if err = tx.Commit(); err != nil { 227 l.Error("tx.Commit failed", "err", err) 228 return ErrDatabaseFail 229 } 230 231 // TODO: notify EditIssue 232 233 return nil 234} 235 236func (s *Service) DeleteIssue(ctx context.Context, issue *models.Issue) error { 237 l := s.logger.With("method", "DeleteIssue") 238 sess, ok := session.FromContext(ctx) 239 if !ok { 240 l.Error("user session is missing in context") 241 return ErrForbidden 242 } 243 atpclient := sess.AtpClient 244 l = l.With("did", sess.User.Did) 245 246 if sess.User.Did != issue.Did { 247 l.Error("only author can edit the issue") 248 return ErrForbidden 249 } 250 251 tx, err := s.db.BeginTx(ctx, nil) 252 if err != nil { 253 l.Error("db.BeginTx failed", "err", err) 254 return ErrDatabaseFail 255 } 256 defer tx.Rollback() 257 258 if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil { 259 l.Error("db.DeleteIssues failed", "err", err) 260 return ErrDatabaseFail 261 } 262 263 _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{ 264 Collection: tangled.RepoIssueNSID, 265 Repo: issue.Did, 266 Rkey: issue.Rkey, 267 }) 268 if err != nil { 269 l.Error("atproto.RepoDeleteRecord failed", "err", err) 270 return ErrPDSFail 271 } 272 273 if err := tx.Commit(); err != nil { 274 l.Error("tx.Commit failed", "err", err) 275 return ErrDatabaseFail 276 } 277 278 s.notifier.DeleteIssue(ctx, issue) 279 return nil 280}