diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2018-05-16 22:01:55 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-16 22:01:55 +0800 |
commit | 24941a10464dc27eaebafda2a208fa827b74ff8d (patch) | |
tree | c875dd6b7d659e2dbd926dbd3a04a036f9f41c94 /models | |
parent | 188fe6c301f9c44d569b75cb339d6a6b3f6e03ad (diff) | |
download | gitea-24941a10464dc27eaebafda2a208fa827b74ff8d.tar.gz gitea-24941a10464dc27eaebafda2a208fa827b74ff8d.zip |
Add more webhooks support and refactor webhook templates directory (#3929)
* add more webhook support
* move hooks templates to standalone dir and add more webhooks ui
* fix tests
* update vendor checksum
* add more webhook support
* move hooks templates to standalone dir and add more webhooks ui
* fix tests
* update vendor checksum
* update vendor
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
* load attributes when created release
* update comparsion doc
Diffstat (limited to 'models')
-rw-r--r-- | models/action.go | 20 | ||||
-rw-r--r-- | models/issue_comment.go | 110 | ||||
-rw-r--r-- | models/issue_milestone.go | 47 | ||||
-rw-r--r-- | models/issue_milestone_test.go | 2 | ||||
-rw-r--r-- | models/release.go | 22 | ||||
-rw-r--r-- | models/repo.go | 11 | ||||
-rw-r--r-- | models/webhook.go | 112 | ||||
-rw-r--r-- | models/webhook_dingtalk.go | 137 | ||||
-rw-r--r-- | models/webhook_discord.go | 186 | ||||
-rw-r--r-- | models/webhook_slack.go | 126 | ||||
-rw-r--r-- | models/webhook_test.go | 2 |
11 files changed, 721 insertions, 54 deletions
diff --git a/models/action.go b/models/action.go index 4f357cb2c5..c3ed9c7c02 100644 --- a/models/action.go +++ b/models/action.go @@ -618,6 +618,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { case ActionDeleteBranch: // Delete Branch isHookEventPush = true + if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{ + Ref: refName, + RefType: "branch", + PusherType: api.PusherTypeUser, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err) + } + case ActionPushTag: // Create isHookEventPush = true @@ -640,6 +650,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { } case ActionDeleteTag: // Delete Tag isHookEventPush = true + + if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{ + Ref: refName, + RefType: "tag", + PusherType: api.PusherTypeUser, + Repo: apiRepo, + Sender: apiPusher, + }); err != nil { + return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err) + } } if isHookEventPush { diff --git a/models/issue_comment.go b/models/issue_comment.go index 2c5875c29c..ad200934bc 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -83,9 +83,10 @@ const ( type Comment struct { ID int64 `xorm:"pk autoincr"` Type CommentType - PosterID int64 `xorm:"INDEX"` - Poster *User `xorm:"-"` - IssueID int64 `xorm:"INDEX"` + PosterID int64 `xorm:"INDEX"` + Poster *User `xorm:"-"` + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` LabelID int64 Label *Label `xorm:"-"` OldMilestoneID int64 @@ -116,6 +117,15 @@ type Comment struct { ShowTag CommentTag `xorm:"-"` } +// LoadIssue loads issue from database +func (c *Comment) LoadIssue() (err error) { + if c.Issue != nil { + return nil + } + c.Issue, err = GetIssueByID(c.IssueID) + return +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (c *Comment) AfterLoad(session *xorm.Session) { var err error @@ -146,40 +156,40 @@ func (c *Comment) AfterDelete() { // HTMLURL formats a URL-string to the issue-comment func (c *Comment) HTMLURL() string { - issue, err := GetIssueByID(c.IssueID) + err := c.LoadIssue() if err != nil { // Silently dropping errors :unamused: - log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) + log.Error(4, "LoadIssue(%d): %v", c.IssueID, err) return "" } - return fmt.Sprintf("%s#%s", issue.HTMLURL(), c.HashTag()) + return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag()) } // IssueURL formats a URL-string to the issue func (c *Comment) IssueURL() string { - issue, err := GetIssueByID(c.IssueID) + err := c.LoadIssue() if err != nil { // Silently dropping errors :unamused: - log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) + log.Error(4, "LoadIssue(%d): %v", c.IssueID, err) return "" } - if issue.IsPull { + if c.Issue.IsPull { return "" } - return issue.HTMLURL() + return c.Issue.HTMLURL() } // PRURL formats a URL-string to the pull-request func (c *Comment) PRURL() string { - issue, err := GetIssueByID(c.IssueID) + err := c.LoadIssue() if err != nil { // Silently dropping errors :unamused: - log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) + log.Error(4, "LoadIssue(%d): %v", c.IssueID, err) return "" } - if !issue.IsPull { + if !c.Issue.IsPull { return "" } - return issue.HTMLURL() + return c.Issue.HTMLURL() } // APIFormat converts a Comment to the api.Comment format @@ -196,9 +206,14 @@ func (c *Comment) APIFormat() *api.Comment { } } +// CommentHashTag returns unique hash tag for comment id. +func CommentHashTag(id int64) string { + return fmt.Sprintf("issuecomment-%d", id) +} + // HashTag returns unique hash tag for comment. func (c *Comment) HashTag() string { - return "issuecomment-" + com.ToStr(c.ID) + return CommentHashTag(c.ID) } // EventTag returns unique event hash tag for comment. @@ -576,7 +591,7 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { // CreateIssueComment creates a plain issue comment. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { - return CreateComment(&CreateCommentOptions{ + comment, err := CreateComment(&CreateCommentOptions{ Type: CommentTypeComment, Doer: doer, Repo: repo, @@ -584,6 +599,21 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri Content: content, Attachments: attachments, }) + if err != nil { + return nil, fmt.Errorf("CreateComment: %v", err) + } + + mode, _ := AccessLevel(doer.ID, repo) + if err = PrepareWebhooks(repo, HookEventIssueComment, &api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: issue.APIFormat(), + Comment: comment.APIFormat(), + Repository: repo.APIFormat(mode), + Sender: doer.APIFormat(), + }); err != nil { + log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) + } + return comment, nil } // CreateRefComment creates a commit reference comment to issue. @@ -696,17 +726,41 @@ func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) { } // UpdateComment updates information of comment. -func UpdateComment(c *Comment) error { +func UpdateComment(doer *User, c *Comment, oldContent string) error { if _, err := x.ID(c.ID).AllCols().Update(c); err != nil { return err } else if c.Type == CommentTypeComment { UpdateIssueIndexer(c.IssueID) } + + if err := c.LoadIssue(); err != nil { + return err + } + if err := c.Issue.LoadAttributes(); err != nil { + return err + } + + mode, _ := AccessLevel(doer.ID, c.Issue.Repo) + if err := PrepareWebhooks(c.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{ + Action: api.HookIssueCommentEdited, + Issue: c.Issue.APIFormat(), + Comment: c.APIFormat(), + Changes: &api.ChangesPayload{ + Body: &api.ChangesFromPayload{ + From: oldContent, + }, + }, + Repository: c.Issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }); err != nil { + log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err) + } + return nil } // DeleteComment deletes the comment -func DeleteComment(comment *Comment) error { +func DeleteComment(doer *User, comment *Comment) error { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { @@ -733,5 +787,25 @@ func DeleteComment(comment *Comment) error { } else if comment.Type == CommentTypeComment { UpdateIssueIndexer(comment.IssueID) } + + if err := comment.LoadIssue(); err != nil { + return err + } + if err := comment.Issue.LoadAttributes(); err != nil { + return err + } + + mode, _ := AccessLevel(doer.ID, comment.Issue.Repo) + + if err := PrepareWebhooks(comment.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{ + Action: api.HookIssueCommentDeleted, + Issue: comment.Issue.APIFormat(), + Comment: comment.APIFormat(), + Repository: comment.Issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }); err != nil { + log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) + } + return nil } diff --git a/models/issue_milestone.go b/models/issue_milestone.go index 8de1f97571..be55dc4f5b 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -5,6 +5,9 @@ package models import ( + "fmt" + + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" api "code.gitea.io/sdk/gitea" @@ -358,7 +361,49 @@ func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil { return err } - return sess.Commit() + + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + var hookAction api.HookIssueAction + if issue.MilestoneID > 0 { + hookAction = api.HookIssueMilestoned + } else { + hookAction = api.HookIssueDemilestoned + } + + if err = issue.LoadAttributes(); err != nil { + return err + } + + mode, _ := AccessLevel(doer.ID, issue.Repo) + if issue.IsPull { + err = issue.PullRequest.LoadIssue() + if err != nil { + log.Error(2, "LoadIssue: %v", err) + return + } + err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{ + Action: hookAction, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } else { + err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{ + Action: hookAction, + Index: issue.Index, + Issue: issue.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } + return nil } // DeleteMilestoneByRepoID deletes a milestone from a repository. diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go index c9b53f4f4a..3ea63d2d6b 100644 --- a/models/issue_milestone_test.go +++ b/models/issue_milestone_test.go @@ -232,6 +232,8 @@ func TestChangeMilestoneAssign(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue) doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) + assert.NotNil(t, issue) + assert.NotNil(t, doer) oldMilestoneID := issue.MilestoneID issue.MilestoneID = 2 diff --git a/models/release.go b/models/release.go index 586f494e7d..bc0260c71d 100644 --- a/models/release.go +++ b/models/release.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -190,8 +191,27 @@ func CreateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []stri } err = addReleaseAttachments(rel.ID, attachmentUUIDs) + if err != nil { + return err + } - return err + if !rel.IsDraft { + if err := rel.LoadAttributes(); err != nil { + log.Error(2, "LoadAttributes: %v", err) + } else { + mode, _ := AccessLevel(rel.PublisherID, rel.Repo) + if err := PrepareWebhooks(rel.Repo, HookEventRelease, &api.ReleasePayload{ + Action: api.HookReleasePublished, + Release: rel.APIFormat(), + Repository: rel.Repo.APIFormat(mode), + Sender: rel.Publisher.APIFormat(), + }); err != nil { + log.Error(2, "PrepareWebhooks: %v", err) + } + } + } + + return nil } // GetRelease returns release by given ID. diff --git a/models/repo.go b/models/repo.go index 4a7eb859c4..f5ec1a9fdd 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2456,6 +2456,17 @@ func ForkRepository(doer, u *User, oldRepo *Repository, name, desc string) (_ *R return nil, err } + oldMode, _ := AccessLevel(doer.ID, oldRepo) + mode, _ := AccessLevel(doer.ID, repo) + + if err = PrepareWebhooks(oldRepo, HookEventFork, &api.ForkPayload{ + Forkee: repo.APIFormat(mode), + Repo: oldRepo.APIFormat(oldMode), + Sender: doer.APIFormat(), + }); err != nil { + log.Error(2, "PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) + } + if err = repo.UpdateSize(); err != nil { log.Error(4, "Failed to update size for repository: %v", err) } diff --git a/models/webhook.go b/models/webhook.go index 62db84f86a..c44ca2960d 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -66,10 +66,15 @@ func IsValidHookContentType(name string) bool { // HookEvents is a set of web hook events type HookEvents struct { - Create bool `json:"create"` - Push bool `json:"push"` - PullRequest bool `json:"pull_request"` - Repository bool `json:"repository"` + Create bool `json:"create"` + Delete bool `json:"delete"` + Fork bool `json:"fork"` + Issues bool `json:"issues"` + IssueComment bool `json:"issue_comment"` + Push bool `json:"push"` + PullRequest bool `json:"pull_request"` + Repository bool `json:"repository"` + Release bool `json:"release"` } // HookEvent represents events that will delivery hook. @@ -155,6 +160,30 @@ func (w *Webhook) HasCreateEvent() bool { (w.ChooseEvents && w.HookEvents.Create) } +// HasDeleteEvent returns true if hook enabled delete event. +func (w *Webhook) HasDeleteEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Delete) +} + +// HasForkEvent returns true if hook enabled fork event. +func (w *Webhook) HasForkEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Fork) +} + +// HasIssuesEvent returns true if hook enabled issues event. +func (w *Webhook) HasIssuesEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Issues) +} + +// HasIssueCommentEvent returns true if hook enabled issue_comment event. +func (w *Webhook) HasIssueCommentEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.IssueComment) +} + // HasPushEvent returns true if hook enabled push event. func (w *Webhook) HasPushEvent() bool { return w.PushOnly || w.SendEverything || @@ -167,23 +196,46 @@ func (w *Webhook) HasPullRequestEvent() bool { (w.ChooseEvents && w.HookEvents.PullRequest) } +// HasReleaseEvent returns if hook enabled release event. +func (w *Webhook) HasReleaseEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Release) +} + // HasRepositoryEvent returns if hook enabled repository event. func (w *Webhook) HasRepositoryEvent() bool { return w.SendEverything || (w.ChooseEvents && w.HookEvents.Repository) } +func (w *Webhook) eventCheckers() []struct { + has func() bool + typ HookEventType +} { + return []struct { + has func() bool + typ HookEventType + }{ + {w.HasCreateEvent, HookEventCreate}, + {w.HasDeleteEvent, HookEventDelete}, + {w.HasForkEvent, HookEventFork}, + {w.HasPushEvent, HookEventPush}, + {w.HasIssuesEvent, HookEventIssues}, + {w.HasIssueCommentEvent, HookEventIssueComment}, + {w.HasPullRequestEvent, HookEventPullRequest}, + {w.HasRepositoryEvent, HookEventRepository}, + {w.HasReleaseEvent, HookEventRelease}, + } +} + // EventsArray returns an array of hook events func (w *Webhook) EventsArray() []string { - events := make([]string, 0, 3) - if w.HasCreateEvent() { - events = append(events, "create") - } - if w.HasPushEvent() { - events = append(events, "push") - } - if w.HasPullRequestEvent() { - events = append(events, "pull_request") + events := make([]string, 0, 7) + + for _, c := range w.eventCheckers() { + if c.has() { + events = append(events, string(c.typ)) + } } return events } @@ -373,10 +425,15 @@ type HookEventType string // Types of hook events const ( - HookEventCreate HookEventType = "create" - HookEventPush HookEventType = "push" - HookEventPullRequest HookEventType = "pull_request" - HookEventRepository HookEventType = "repository" + HookEventCreate HookEventType = "create" + HookEventDelete HookEventType = "delete" + HookEventFork HookEventType = "fork" + HookEventPush HookEventType = "push" + HookEventIssues HookEventType = "issues" + HookEventIssueComment HookEventType = "issue_comment" + HookEventPullRequest HookEventType = "pull_request" + HookEventRepository HookEventType = "repository" + HookEventRelease HookEventType = "release" ) // HookRequest represents hook task request information. @@ -488,22 +545,11 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay } func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error { - switch event { - case HookEventCreate: - if !w.HasCreateEvent() { - return nil - } - case HookEventPush: - if !w.HasPushEvent() { - return nil - } - case HookEventPullRequest: - if !w.HasPullRequestEvent() { - return nil - } - case HookEventRepository: - if !w.HasRepositoryEvent() { - return nil + for _, e := range w.eventCheckers() { + if event == e.typ { + if !e.has() { + return nil + } } } diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go index 719ffcae73..7eb189f9bb 100644 --- a/models/webhook_dingtalk.go +++ b/models/webhook_dingtalk.go @@ -49,6 +49,38 @@ func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { }, nil } +func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: fmt.Sprintf("view branch %s", refName), + SingleURL: p.Repo.HTMLURL + "/src/" + refName, + }, + }, nil +} + +func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) { + title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: fmt.Sprintf("view forked repo %s", p.Repo.FullName), + SingleURL: p.Repo.HTMLURL, + }, + }, nil +} + func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { var ( branchName = git.RefEndName(p.Ref) @@ -98,6 +130,80 @@ func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { }, nil } +func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueClosed: + title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueAssigned: + title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName, + p.Issue.Assignee.UserName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: text, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: p.Issue.URL, + }, + }, nil +} + +func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) { + title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title) + url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) + var content string + switch p.Action { + case api.HookIssueCommentCreated: + title = "New comment: " + title + content = p.Comment.Body + case api.HookIssueCommentEdited: + title = "Comment edited: " + title + content = p.Comment.Body + case api.HookIssueCommentDeleted: + title = "Comment deleted: " + title + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: content, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: url, + }, + }, nil +} + func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { var text, title string switch p.Action { @@ -182,6 +288,27 @@ func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, e return nil, nil } +func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) { + var title, url string + switch p.Action { + case api.HookReleasePublished: + title = fmt.Sprintf("[%s] Release created", p.Release.TagName) + url = p.Release.URL + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: "view repository", + SingleURL: url, + }, + }, nil + } + + return nil, nil +} + // GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) { s := new(DingtalkPayload) @@ -189,12 +316,22 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*Din switch event { case HookEventCreate: return getDingtalkCreatePayload(p.(*api.CreatePayload)) + case HookEventDelete: + return getDingtalkDeletePayload(p.(*api.DeletePayload)) + case HookEventFork: + return getDingtalkForkPayload(p.(*api.ForkPayload)) + case HookEventIssues: + return getDingtalkIssuesPayload(p.(*api.IssuePayload)) + case HookEventIssueComment: + return getDingtalkIssueCommentPayload(p.(*api.IssueCommentPayload)) case HookEventPush: return getDingtalkPushPayload(p.(*api.PushPayload)) case HookEventPullRequest: return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) case HookEventRepository: return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) + case HookEventRelease: + return getDingtalkReleasePayload(p.(*api.ReleasePayload)) } return s, nil diff --git a/models/webhook_discord.go b/models/webhook_discord.go index 40d9d58992..04ebbc293f 100644 --- a/models/webhook_discord.go +++ b/models/webhook_discord.go @@ -115,6 +115,51 @@ func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordP }, nil } +func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) { + // deleted tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + URL: p.Repo.HTMLURL + "/src/" + refName, + Color: warnColor, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + +func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) { + // fork + title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + URL: p.Repo.HTMLURL, + Color: successColor, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) { var ( branchName = git.RefEndName(p.Ref) @@ -165,6 +210,108 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo }, nil } +func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) { + var text, title string + var color int + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueClosed: + title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + color = failedColor + text = p.Issue.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueAssigned: + title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName, + p.Issue.Assignee.UserName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = successColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: p.Issue.URL, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + +func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) { + title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title) + url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) + content := "" + var color int + switch p.Action { + case api.HookIssueCommentCreated: + title = "New comment: " + title + content = p.Comment.Body + color = successColor + case api.HookIssueCommentEdited: + title = "Comment edited: " + title + content = p.Comment.Body + color = warnColor + case api.HookIssueCommentDeleted: + title = "Comment deleted: " + title + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + color = warnColor + } + + return &DiscordPayload{ + Username: discord.Username, + AvatarURL: discord.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: content, + URL: url, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) { var text, title string var color int @@ -267,6 +414,35 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (* }, nil } +func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) { + var title, url string + var color int + switch p.Action { + case api.HookReleasePublished: + title = fmt.Sprintf("[%s] Release created", p.Release.TagName) + url = p.Release.URL + color = successColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: fmt.Sprintf("%s", p.Release.Note), + URL: url, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + // GetDiscordPayload converts a discord webhook into a DiscordPayload func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) { s := new(DiscordPayload) @@ -279,12 +455,22 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc switch event { case HookEventCreate: return getDiscordCreatePayload(p.(*api.CreatePayload), discord) + case HookEventDelete: + return getDiscordDeletePayload(p.(*api.DeletePayload), discord) + case HookEventFork: + return getDiscordForkPayload(p.(*api.ForkPayload), discord) + case HookEventIssues: + return getDiscordIssuesPayload(p.(*api.IssuePayload), discord) + case HookEventIssueComment: + return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord) case HookEventPush: return getDiscordPushPayload(p.(*api.PushPayload), discord) case HookEventPullRequest: return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) case HookEventRepository: return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) + case HookEventRelease: + return getDiscordReleasePayload(p.(*api.ReleasePayload), discord) } return s, nil diff --git a/models/webhook_slack.go b/models/webhook_slack.go index 256819adc5..7b18fe3278 100644 --- a/models/webhook_slack.go +++ b/models/webhook_slack.go @@ -106,6 +106,122 @@ func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayloa }, nil } +// getSlackDeletePayload composes Slack payload for delete a branch or tag. +func getSlackDeletePayload(p *api.DeletePayload, slack *SlackMeta) (*SlackPayload, error) { + refName := git.RefEndName(p.Ref) + repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + }, nil +} + +// getSlackForkPayload composes Slack payload for forked by a repository. +func getSlackForkPayload(p *api.ForkPayload, slack *SlackMeta) (*SlackPayload, error) { + baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) + text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + }, nil +} + +func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload, error) { + senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index), + fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)) + var text, title, attachmentText string + switch p.Action { + case api.HookIssueOpened: + text = fmt.Sprintf("[%s] Issue submitted by %s", p.Repository.FullName, senderLink) + title = titleLink + attachmentText = SlackTextFormatter(p.Issue.Body) + case api.HookIssueClosed: + text = fmt.Sprintf("[%s] Issue closed: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueReOpened: + text = fmt.Sprintf("[%s] Issue re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueEdited: + text = fmt.Sprintf("[%s] Issue edited: %s by %s", p.Repository.FullName, titleLink, senderLink) + attachmentText = SlackTextFormatter(p.Issue.Body) + case api.HookIssueAssigned: + text = fmt.Sprintf("[%s] Issue assigned to %s: %s by %s", p.Repository.FullName, + SlackLinkFormatter(setting.AppURL+p.Issue.Assignee.UserName, p.Issue.Assignee.UserName), + titleLink, senderLink) + case api.HookIssueUnassigned: + text = fmt.Sprintf("[%s] Issue unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueLabelUpdated: + text = fmt.Sprintf("[%s] Issue labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueLabelCleared: + text = fmt.Sprintf("[%s] Issue labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueSynchronized: + text = fmt.Sprintf("[%s] Issue synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink) + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Title: title, + Text: attachmentText, + }}, + }, nil +} + +func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) { + senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)), + fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)) + var text, title, attachmentText string + switch p.Action { + case api.HookIssueCommentCreated: + text = fmt.Sprintf("[%s] New comment created by %s", p.Repository.FullName, senderLink) + title = titleLink + attachmentText = SlackTextFormatter(p.Comment.Body) + case api.HookIssueCommentEdited: + text = fmt.Sprintf("[%s] Comment edited by %s", p.Repository.FullName, senderLink) + title = titleLink + attachmentText = SlackTextFormatter(p.Comment.Body) + case api.HookIssueCommentDeleted: + text = fmt.Sprintf("[%s] Comment deleted by %s", p.Repository.FullName, senderLink) + title = SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index), + fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)) + attachmentText = SlackTextFormatter(p.Comment.Body) + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Title: title, + Text: attachmentText, + }}, + }, nil +} + +func getSlackReleasePayload(p *api.ReleasePayload, slack *SlackMeta) (*SlackPayload, error) { + repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.Name) + refLink := SlackLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName) + text := fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName) + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + }, nil +} + func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) { // n new commits var ( @@ -238,12 +354,22 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP switch event { case HookEventCreate: return getSlackCreatePayload(p.(*api.CreatePayload), slack) + case HookEventDelete: + return getSlackDeletePayload(p.(*api.DeletePayload), slack) + case HookEventFork: + return getSlackForkPayload(p.(*api.ForkPayload), slack) + case HookEventIssues: + return getSlackIssuesPayload(p.(*api.IssuePayload), slack) + case HookEventIssueComment: + return getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack) case HookEventPush: return getSlackPushPayload(p.(*api.PushPayload), slack) case HookEventPullRequest: return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) case HookEventRepository: return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) + case HookEventRelease: + return getSlackReleasePayload(p.(*api.ReleasePayload), slack) } return s, nil diff --git a/models/webhook_test.go b/models/webhook_test.go index eeae7efbcb..50106a3792 100644 --- a/models/webhook_test.go +++ b/models/webhook_test.go @@ -73,7 +73,7 @@ func TestWebhook_UpdateEvent(t *testing.T) { } func TestWebhook_EventsArray(t *testing.T) { - assert.Equal(t, []string{"create", "push", "pull_request"}, + assert.Equal(t, []string{"create", "delete", "fork", "push", "issues", "issue_comment", "pull_request", "repository", "release"}, (&Webhook{ HookEvent: &HookEvent{SendEverything: true}, }).EventsArray(), |