Monorepo for Tangled tangled.org
at sl/shared-stacks 239 lines 5.6 kB view raw
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}