From 0bc995889f3784eeca82ad9341c299bc868d2634 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 24 Oct 2022 21:39:16 +0800 Subject: [PATCH] Finish bots events notifications --- modules/notification/bots/bots.go | 459 +++++++++++++++++++++++++----- 1 file changed, 390 insertions(+), 69 deletions(-) diff --git a/modules/notification/bots/bots.go b/modules/notification/bots/bots.go index ef07f2323f..094b3c9929 100644 --- a/modules/notification/bots/bots.go +++ b/modules/notification/bots/bots.go @@ -11,7 +11,10 @@ import ( bots_model "code.gitea.io/gitea/models/bots" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" @@ -40,47 +43,32 @@ func NewNotifier() base.Notifier { return &botsNotifier{} } -func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEventType, payload string) { - err := issue.LoadRepo(db.DefaultContext) - if err != nil { - log.Error("issue.LoadRepo: %v", err) - return - } - if issue.Repo.IsEmpty || issue.Repo.IsArchived { - return - } - - ref := issue.Ref - if ref == "" { - ref = issue.Repo.DefaultBranch - } - notify(issue.Repo, doer, payload, ref, evt) -} - -func notify(repo *repo_model.Repository, doer *user_model.User, payload, ref string, evt webhook.HookEventType) { +func notify(repo *repo_model.Repository, doer *user_model.User, ref string, evt webhook.HookEventType, payload api.Payloader) error { gitRepo, err := git.OpenRepository(context.Background(), repo.RepoPath()) if err != nil { - log.Error("issue.LoadRepo: %v", err) - return + return fmt.Errorf("git.OpenRepository: %v", err) } defer gitRepo.Close() // Get the commit object for the ref commit, err := gitRepo.GetCommit(ref) if err != nil { - log.Error("gitRepo.GetCommit: %v", err) - return + return fmt.Errorf("gitRepo.GetCommit: %v", err) } workflows, err := bots_module.DetectWorkflows(commit, evt) if err != nil { - log.Error("DetectWorkflows: %v", err) - return + return fmt.Errorf("DetectWorkflows: %v", err) } if len(workflows) == 0 { log.Trace("repo %s with commit %s couldn't find workflows", repo.RepoPath(), commit.ID) - return + return nil + } + + p, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("json.Marshal: %v", err) } for id, content := range workflows { @@ -92,11 +80,11 @@ func notify(repo *repo_model.Repository, doer *user_model.User, payload, ref str Ref: ref, CommitSHA: commit.ID.String(), Event: evt, - EventPayload: payload, + EventPayload: string(p), Status: bots_model.StatusWaiting, } if len(run.Title) > 255 { - run.Title = run.Title[:255] + run.Title = run.Title[:255] // FIXME: we should use a better method to cut title } jobs, err := jobparser.Parse(content) // TODO: parse with options if err != nil { @@ -107,49 +95,175 @@ func notify(repo *repo_model.Repository, doer *user_model.User, payload, ref str log.Error("InsertRun: %v", err) } } + return nil } -// TODO: implement all events -func (a *botsNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) { - payload := map[string]interface{}{ - "issue": map[string]interface{}{ - "number": issue.Index, - }, - } - bs, err := json.Marshal(payload) - if err != nil { - log.Error("NotifyNewIssue: %v", err) +// NotifyNewIssue notifies issue created event +func (a *botsNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []*user_model.User) { + if err := issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("issue.LoadRepo: %v", err) return } - notifyIssue(issue, issue.Poster, webhook.HookEventIssues, string(bs)) + if err := issue.LoadPoster(); err != nil { + log.Error("issue.LoadPoster: %v", err) + return + } + + mode, _ := access_model.AccessLevel(issue.Poster, issue.Repo) + if err := notify(issue.Repo, issue.Poster, issue.Repo.DefaultBranch, + webhook.HookEventIssues, &api.IssuePayload{ + Action: api.HookIssueOpened, + Index: issue.Index, + Issue: convert.ToAPIIssue(issue), + Repository: convert.ToRepo(issue.Repo, mode), + Sender: convert.ToUser(issue.Poster, nil), + }); err != nil { + log.Error("notify: %v", err) + } } // NotifyIssueChangeStatus notifies close or reopen issue to notifiers -func (a *botsNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { +func (a *botsNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { + mode, _ := access_model.AccessLevel(issue.Poster, issue.Repo) + var err error + if issue.IsPull { + if err = issue.LoadPullRequest(); err != nil { + log.Error("LoadPullRequest: %v", err) + return + } + // Merge pull request calls issue.changeStatus so we need to handle separately. + apiPullRequest := &api.PullRequestPayload{ + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, issue.PullRequest, nil), + Repository: convert.ToRepo(issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + } + if isClosed { + apiPullRequest.Action = api.HookIssueClosed + } else { + apiPullRequest.Action = api.HookIssueReOpened + } + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventPullRequest, apiPullRequest) + } else { + apiIssue := &api.IssuePayload{ + Index: issue.Index, + Issue: convert.ToAPIIssue(issue), + Repository: convert.ToRepo(issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + } + if isClosed { + apiIssue.Action = api.HookIssueClosed + } else { + apiIssue.Action = api.HookIssueReOpened + } + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventIssues, apiIssue) + } + if err != nil { + log.Error("PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err) + } } -func (a *botsNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, - addedLabels []*models.Label, removedLabels []*models.Label, +func (a *botsNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, + addedLabels []*issues_model.Label, removedLabels []*issues_model.Label, ) { - payload := map[string]interface{}{ - "issue": map[string]interface{}{ - "number": issue.Index, - }, - } - bs, err := json.Marshal(payload) - if err != nil { - log.Error("NotifyNewIssue: %v", err) + var err error + if err = issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("LoadRepo: %v", err) return } - notifyIssue(issue, doer, webhook.HookEventIssueLabel, string(bs)) + + if err = issue.LoadPoster(); err != nil { + log.Error("LoadPoster: %v", err) + return + } + + mode, _ := access_model.AccessLevel(issue.Poster, issue.Repo) + if issue.IsPull { + if err = issue.LoadPullRequest(); err != nil { + log.Error("loadPullRequest: %v", err) + return + } + if err = issue.PullRequest.LoadIssue(); err != nil { + log.Error("LoadIssue: %v", err) + return + } + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventPullRequestLabel, &api.PullRequestPayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, issue.PullRequest, nil), + Repository: convert.ToRepo(issue.Repo, perm.AccessModeNone), + Sender: convert.ToUser(doer, nil), + }) + } else { + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventIssueLabel, &api.IssuePayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + Issue: convert.ToAPIIssue(issue), + Repository: convert.ToRepo(issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + }) + } + if err != nil { + log.Error("bostNotifier [is_pull: %v]: %v", issue.IsPull, err) + } } // NotifyCreateIssueComment notifies comment on an issue to notifiers func (a *botsNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, - issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { + issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, +) { + mode, _ := access_model.AccessLevel(doer, repo) + + var err error + if issue.IsPull { + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventPullRequestComment, &api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: convert.ToAPIIssue(issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: true, + }) + } else { + err = notify(issue.Repo, doer, issue.Repo.DefaultBranch, webhook.HookEventIssueComment, &api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: convert.ToAPIIssue(issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: false, + }) + } + + if err != nil { + log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) + } } -func (a *botsNotifier) NotifyNewPullRequest(pull *models.PullRequest, mentions []*user_model.User) { +func (a *botsNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, mentions []*user_model.User) { + if err := pull.LoadIssue(); err != nil { + log.Error("pull.LoadIssue: %v", err) + return + } + if err := pull.Issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("pull.Issue.LoadRepo: %v", err) + return + } + if err := pull.Issue.LoadPoster(); err != nil { + log.Error("pull.Issue.LoadPoster: %v", err) + return + } + + mode, _ := access_model.AccessLevel(pull.Issue.Poster, pull.Issue.Repo) + if err := notify(pull.Issue.Repo, pull.Issue.Poster, pull.Issue.Repo.DefaultBranch, webhook.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueOpened, + Index: pull.Issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pull, nil), + Repository: convert.ToRepo(pull.Issue.Repo, mode), + Sender: convert.ToUser(pull.Issue.Poster, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } } func (a *botsNotifier) NotifyRenameRepository(doer *user_model.User, repo *repo_model.Repository, oldRepoName string) { @@ -159,19 +273,127 @@ func (a *botsNotifier) NotifyTransferRepository(doer *user_model.User, repo *rep } func (a *botsNotifier) NotifyCreateRepository(doer *user_model.User, u *user_model.User, repo *repo_model.Repository) { + if err := notify(repo, doer, repo.DefaultBranch, + webhook.HookEventRepository, + &api.RepositoryPayload{ + Action: api.HookRepoCreated, + Repository: convert.ToRepo(repo, perm.AccessModeOwner), + Organization: convert.ToUser(u, nil), + Sender: convert.ToUser(doer, nil), + }); err != nil { + log.Error("Bots Notifier [repo_id: %d]: %v", repo.ID, err) + } } func (a *botsNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, repo *repo_model.Repository) { + oldMode, _ := access_model.AccessLevel(doer, oldRepo) + mode, _ := access_model.AccessLevel(doer, repo) + + // forked webhook + if err := notify(oldRepo, doer, oldRepo.DefaultBranch, webhook.HookEventFork, &api.ForkPayload{ + Forkee: convert.ToRepo(oldRepo, oldMode), + Repo: convert.ToRepo(repo, mode), + Sender: convert.ToUser(doer, nil), + }); err != nil { + log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) + } + + u := repo.MustOwner() + + // Add to hook queue for created repo after session commit. + if u.IsOrganization() { + if err := notify(repo, doer, oldRepo.DefaultBranch, webhook.HookEventRepository, &api.RepositoryPayload{ + Action: api.HookRepoCreated, + Repository: convert.ToRepo(repo, perm.AccessModeOwner), + Organization: convert.ToUser(u, nil), + Sender: convert.ToUser(doer, nil), + }); err != nil { + log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) + } + } } -func (a *botsNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment, mentions []*user_model.User) { +func (a *botsNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { + var reviewHookType webhook.HookEventType + + switch review.Type { + case issues_model.ReviewTypeApprove: + reviewHookType = webhook.HookEventPullRequestReviewApproved + case issues_model.ReviewTypeComment: + reviewHookType = webhook.HookEventPullRequestComment + case issues_model.ReviewTypeReject: + reviewHookType = webhook.HookEventPullRequestReviewRejected + default: + // unsupported review webhook type here + log.Error("Unsupported review webhook type") + return + } + + if err := pr.LoadIssue(); err != nil { + log.Error("pr.LoadIssue: %v", err) + return + } + + mode, err := access_model.AccessLevel(review.Issue.Poster, review.Issue.Repo) + if err != nil { + log.Error("models.AccessLevel: %v", err) + return + } + if err := notify(review.Issue.Repo, review.Reviewer, review.CommitID, reviewHookType, &api.PullRequestPayload{ + Action: api.HookIssueReviewed, + Index: review.Issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), + Repository: convert.ToRepo(review.Issue.Repo, mode), + Sender: convert.ToUser(review.Reviewer, nil), + Review: &api.ReviewPayload{ + Type: string(reviewHookType), + Content: review.Content, + }, + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } } -func (*botsNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { +func (*botsNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + // Reload pull request information. + if err := pr.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.LoadIssue(); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + log.Error("pr.Issue.LoadRepo: %v", err) + return + } + + mode, err := access_model.AccessLevel(doer, pr.Issue.Repo) + if err != nil { + log.Error("models.AccessLevel: %v", err) + return + } + + // Merge pull request calls issue.changeStatus so we need to handle separately. + apiPullRequest := &api.PullRequestPayload{ + Index: pr.Issue.Index, + PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), + Repository: convert.ToRepo(pr.Issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + Action: api.HookIssueClosed, + } + + err = notify(pr.Issue.Repo, doer, pr.MergedCommitID, webhook.HookEventPullRequest, apiPullRequest) + if err != nil { + log.Error("PrepareWebhooks: %v", err) + } } func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { - ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyPushCommits User: %s[%d] in %s[%d]", pusher.Name, pusher.ID, repo.FullName(), repo.ID)) + ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("botsNofiter.NotifyPushCommits User: %s[%d] in %s[%d]", pusher.Name, pusher.ID, repo.FullName(), repo.ID)) defer finished() apiPusher := convert.ToUser(pusher, nil) @@ -181,7 +403,7 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod return } - payload := &api.PushPayload{ + notify(repo, pusher, opts.RefFullName, webhook.HookEventPush, &api.PushPayload{ Ref: opts.RefFullName, Before: opts.OldCommitID, After: opts.NewCommitID, @@ -191,31 +413,130 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod Repo: convert.ToRepo(repo, perm.AccessModeOwner), Pusher: apiPusher, Sender: apiPusher, - } - - bs, err := json.Marshal(payload) - if err != nil { - log.Error("json.Marshal(payload) failed: %v", err) - return - } - - notify(repo, pusher, string(bs), opts.RefFullName, webhook.HookEventPush) + }) } -func (a *botsNotifier) NotifyCreateRef(doer *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { +func (a *botsNotifier) NotifyCreateRef(pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { + apiPusher := convert.ToUser(pusher, nil) + apiRepo := convert.ToRepo(repo, perm.AccessModeNone) + refName := git.RefEndName(refFullName) + + if err := notify(repo, pusher, refName, webhook.HookEventCreate, &api.CreatePayload{ + Ref: refName, + Sha: refID, + RefType: refType, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } } -func (a *botsNotifier) NotifyDeleteRef(doer *user_model.User, repo *repo_model.Repository, refType, refFullName string) { +func (a *botsNotifier) NotifyDeleteRef(pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) { + apiPusher := convert.ToUser(pusher, nil) + apiRepo := convert.ToRepo(repo, perm.AccessModeNone) + refName := git.RefEndName(refFullName) + + if err := notify(repo, pusher, refName, webhook.HookEventDelete, &api.DeletePayload{ + Ref: refName, + RefType: refType, + PusherType: api.PusherTypeUser, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + log.Error("PrepareWebhooks.(delete %s): %v", refType, err) + } } func (a *botsNotifier) NotifySyncPushCommits(pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { + apiPusher := convert.ToUser(pusher, nil) + apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(db.DefaultContext, repo.RepoPath(), repo.HTMLURL()) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + + if err := notify(repo, pusher, apiHeadCommit.ID, webhook.HookEventPush, &api.PushPayload{ + Ref: opts.RefFullName, + Before: opts.OldCommitID, + After: opts.NewCommitID, + CompareURL: setting.AppURL + commits.CompareURL, + Commits: apiCommits, + TotalCommits: commits.Len, + HeadCommit: apiHeadCommit, + Repo: convert.ToRepo(repo, perm.AccessModeOwner), + Pusher: apiPusher, + Sender: apiPusher, + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } } -func (a *botsNotifier) NotifySyncCreateRef(doer *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { +func (a *botsNotifier) NotifySyncCreateRef(pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) { + a.NotifyCreateRef(pusher, repo, refType, refFullName, refID) } -func (a *botsNotifier) NotifySyncDeleteRef(doer *user_model.User, repo *repo_model.Repository, refType, refFullName string) { +func (a *botsNotifier) NotifySyncDeleteRef(pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) { + a.NotifyDeleteRef(pusher, repo, refType, refFullName) +} + +func sendReleaseNofiter(doer *user_model.User, rel *repo_model.Release, ref string, action api.HookReleaseAction) { + if err := rel.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + mode, _ := access_model.AccessLevel(doer, rel.Repo) + if err := notify(rel.Repo, doer, ref, webhook.HookEventRelease, &api.ReleasePayload{ + Action: action, + Release: convert.ToRelease(rel), + Repository: convert.ToRepo(rel.Repo, mode), + Sender: convert.ToUser(doer, nil), + }); err != nil { + log.Error("notify: %v", err) + } } func (a *botsNotifier) NotifyNewRelease(rel *repo_model.Release) { + sendReleaseNofiter(rel.Publisher, rel, rel.Sha1, api.HookReleasePublished) +} + +func (a *botsNotifier) NotifyUpdateRelease(doer *user_model.User, rel *repo_model.Release) { + sendReleaseNofiter(doer, rel, rel.Sha1, api.HookReleaseUpdated) +} + +func (a *botsNotifier) NotifyDeleteRelease(doer *user_model.User, rel *repo_model.Release) { + sendReleaseNofiter(doer, rel, rel.Sha1, api.HookReleaseDeleted) +} + +func (a *botsNotifier) NotifyPackageCreate(doer *user_model.User, pd *packages_model.PackageDescriptor) { + notifyPackage(doer, pd, api.HookPackageCreated) +} + +func (a *botsNotifier) NotifyPackageDelete(doer *user_model.User, pd *packages_model.PackageDescriptor) { + notifyPackage(doer, pd, api.HookPackageDeleted) +} + +func notifyPackage(sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) { + if pd.Repository == nil { + // TODO https://github.com/go-gitea/gitea/pull/17940 + return + } + + ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.notifyPackage Package: %s[%d]", pd.Package.Name, pd.Package.ID)) + defer finished() + + apiPackage, err := convert.ToPackage(ctx, pd, sender) + if err != nil { + log.Error("Error converting package: %v", err) + return + } + + if err := notify(pd.Repository, sender, "", webhook.HookEventPackage, &api.PackagePayload{ + Action: action, + Package: apiPackage, + Sender: convert.ToUser(sender, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } }