package pull import ( "context" "log/slog" "time" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "tangled.org/core/api/tangled" "tangled.org/core/appview/config" "tangled.org/core/appview/db" pulls_indexer "tangled.org/core/appview/indexer/pulls" "tangled.org/core/appview/mentions" "tangled.org/core/appview/models" "tangled.org/core/appview/notify" "tangled.org/core/appview/session" "tangled.org/core/idresolver" "tangled.org/core/rbac" "tangled.org/core/tid" ) type Service struct { config *config.Config db *db.DB enforcer *rbac.Enforcer indexer *pulls_indexer.Indexer logger *slog.Logger notifier notify.Notifier idResolver *idresolver.Resolver mentionsResolver *mentions.Resolver } func NewService( logger *slog.Logger, config *config.Config, db *db.DB, enforcer *rbac.Enforcer, notifier notify.Notifier, idResolver *idresolver.Resolver, mentionsResolver *mentions.Resolver, indexer *pulls_indexer.Indexer, ) Service { return Service{ config, db, enforcer, indexer, logger, notifier, idResolver, mentionsResolver, } } // SubmitPull creates a new PR or resubmits existing PR. // `pull` can be `nil` for creating a new PR. func (s *Service) SubmitPull( ctx context.Context, pull *models.Pull2, target models.PullTarget, source *models.PullSource2, patch, title, body string, ) error { l := s.logger.With("method", "NewPullSubmission") sess := session.FromContext(ctx) if sess == nil { l.Error("user session is missing in context") return ErrForbidden } sessDid := sess.Data.AccountDID l = l.With("did", sessDid) var ( did syntax.DID rkey syntax.RecordKey ) if pull == nil { // new pr did = sessDid rkey = syntax.RecordKey(tid.TID()) } else { // resubmit if sessDid != pull.Did { l.Error("only author can edit the pull") return ErrForbidden } did = pull.Did rkey = pull.Rkey } mentions, references := s.mentionsResolver.Resolve(ctx, body) round := models.PullRound{ Did: did, Rkey: rkey, Target: target, Source: source, Patch: patch, Title: title, Body: body, Mentions: mentions, References: references, Created: time.Now(), } if err := round.Validate(); err != nil { l.Error("validation error", "err", err) return ErrValidationFail } atpclient := sess.APIClient() record := round.AsRecord() var exCid *string if pull != nil { x := pull.CID().String() exCid = &x } resp, err := atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ Collection: tangled.RepoPullNSID, SwapRecord: exCid, Record: &lexutil.LexiconTypeDecoder{ Val: &record, }, }) if err != nil { l.Error("atproto.RepoPutRecord failed", "err", err) return ErrPDSFail } round.Cid = syntax.CID(resp.Cid) tx, err := s.db.BeginTx(ctx, nil) if err != nil { l.Error("db.BeginTx failed", "err", err) return ErrDatabaseFail } defer tx.Rollback() if err := db.NewPullRound(tx, 0, &round); err != nil { l.Error("db.UpdatePull2 failed", "err", err) return ErrDatabaseFail } if err = tx.Commit(); err != nil { l.Error("tx.Commit failed", "err", err) return ErrDatabaseFail } if pull == nil { // s.notifier.NewPull(ctx, &round) } else { pull.Submissions = append(pull.Submissions, &round) // s.notifier.ResubmitPull(ctx, &round) } return nil } func (s *Service) DeletePull(ctx context.Context, pull *models.Pull2) error { l := s.logger.With("method", "DeletePull") sess := session.FromContext(ctx) if sess == nil { l.Error("user session is missing in context") return ErrForbidden } sessDid := sess.Data.AccountDID l = l.With("did", sessDid) if sessDid != pull.Did { l.Error("only author can delete the pull") return ErrForbidden } tx, err := s.db.BeginTx(ctx, nil) if err != nil { l.Error("db.BeginTx failed", "err", err) return ErrDatabaseFail } defer tx.Rollback() if err := db.DeletePull2(tx, pull.AtUri()); err != nil { l.Error("db.DeletePull2 failed", "err", err) return ErrDatabaseFail } atpclient := sess.APIClient() _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{ Collection: tangled.RepoIssueNSID, Repo: pull.Did.String(), Rkey: pull.Rkey.String(), }) if err != nil { l.Error("atproto.RepoDeleteRecord failed", "err", err) return ErrPDSFail } if err := tx.Commit(); err != nil { l.Error("tx.Commit failed", "err", err) return ErrDatabaseFail } pull.State = models.PullDeleted // s.notifier.DeletePull(ctx, pull) return nil } func (s *Service) ListPulls(ctx context.Context, repo *models.Repo, searchOpts models.PullSearchOptions) ([]*models.Pull2, error) { l := s.logger.With("method", "ListPulls") var pulls []*models.Pull2 var err error if searchOpts.Keyword != "" { res, err := s.indexer.Search(ctx, searchOpts) if err != nil { l.Error("failed to search for pulls", "err", err) return nil, ErrIndexerFail } l.Debug("searched pulls with indexer", "count", len(res.Hits)) pulls, err = db.GetPulls2(s.db, db.FilterIn("id", res.Hits)) if err != nil { l.Error("failed to get pulls", "err", err) return nil, ErrDatabaseFail } } else { pulls, err = db.GetPullsPaginated( s.db, searchOpts.Page, db.FilterEq("repo_at", repo.RepoAt()), db.FilterEq("state", searchOpts.State), ) if err != nil { l.Error("failed to get pulls", "err", err) return nil, ErrDatabaseFail } } return pulls, nil }