From: Lunny Xiao Date: Sun, 3 Nov 2019 22:13:25 +0000 (+0800) Subject: Move more webhook codes from models to webhook module (#8802) X-Git-Tag: v1.11.0-rc1~419 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a966a0298ea1a545c383541ca4e72c61de1ed59e;p=gitea.git Move more webhook codes from models to webhook module (#8802) * Move more webhook codes from models to webhook module --- diff --git a/models/webhook.go b/models/webhook.go index d3a8b52d86..7eb17caaf6 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -118,33 +118,6 @@ func (w *Webhook) AfterLoad() { } } -// GetSlackHook returns slack metadata -func (w *Webhook) GetSlackHook() *SlackMeta { - s := &SlackMeta{} - if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetSlackHook(%d): %v", w.ID, err) - } - return s -} - -// GetDiscordHook returns discord metadata -func (w *Webhook) GetDiscordHook() *DiscordMeta { - s := &DiscordMeta{} - if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err) - } - return s -} - -// GetTelegramHook returns telegram metadata -func (w *Webhook) GetTelegramHook() *TelegramMeta { - s := &TelegramMeta{} - if err := json.Unmarshal([]byte(w.Meta), s); err != nil { - log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err) - } - return s -} - // History returns history of webhook by given conditions. func (w *Webhook) History(page int) ([]*HookTask, error) { return HookTasks(w.ID, page) diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go deleted file mode 100644 index 1c6c0a83b8..0000000000 --- a/models/webhook_dingtalk.go +++ /dev/null @@ -1,422 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "encoding/json" - "fmt" - "strings" - - "code.gitea.io/gitea/modules/git" - api "code.gitea.io/gitea/modules/structs" - - dingtalk "github.com/lunny/dingtalk_webhook" -) - -type ( - // DingtalkPayload represents - DingtalkPayload dingtalk.Payload -) - -// SetSecret sets the dingtalk secret -func (p *DingtalkPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the DingtalkPayload to json -func (p *DingtalkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { - // created tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title, - Title: title, - HideAvatar: "0", - SingleTitle: fmt.Sprintf("view ref %s", refName), - SingleURL: p.Repo.HTMLURL + "/src/" + refName, - }, - }, 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 ref %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) - commitDesc string - ) - - var titleLink, linkText string - if len(p.Commits) == 1 { - commitDesc = "1 new commit" - titleLink = p.Commits[0].URL - linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7]) - } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) - titleLink = p.CompareURL - linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7]) - } - if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName - } - - title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) - - var text string - // for each commit, generate attachment text - for i, commit := range p.Commits { - var authorName string - if commit.Author != nil { - authorName = " - " + commit.Author.Name - } - text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n")) + authorName - // add linebreak to each commit but the last - if i < len(p.Commits)-1 { - text += "\n" - } - } - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: text, - Title: title, - HideAvatar: "0", - SingleTitle: linkText, - SingleURL: titleLink, - }, - }, 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] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueLabelCleared: - title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - } - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title + "\r\n\r\n" + text, - //Markdown: "# " + title + "\n" + text, - Title: title, - HideAvatar: "0", - SingleTitle: "view issue", - 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: - if p.IsPull { - title = "New comment on pull request " + title - } else { - title = "New comment on issue " + title - } - content = p.Comment.Body - case api.HookIssueCommentEdited: - if p.IsPull { - title = "Comment edited on pull request " + title - } else { - title = "Comment edited on issue " + title - } - content = p.Comment.Body - case api.HookIssueCommentDeleted: - if p.IsPull { - title = "Comment deleted on pull request " + title - } else { - title = "Comment deleted on issue " + title - } - url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) - content = p.Comment.Body - } - - title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title + "\r\n\r\n" + content, - Title: title, - HideAvatar: "0", - SingleTitle: "view issue comment", - SingleURL: url, - }, - }, nil -} - -func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { - var text, title string - switch p.Action { - case api.HookIssueOpened: - title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueClosed: - if p.PullRequest.HasMerged { - title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - } else { - title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - } - text = p.PullRequest.Body - case api.HookIssueReOpened: - title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueEdited: - title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueAssigned: - list := make([]string, len(p.PullRequest.Assignees)) - for i, user := range p.PullRequest.Assignees { - list[i] = user.UserName - } - title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName, - strings.Join(list, ", "), - p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueUnassigned: - title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueLabelUpdated: - title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueLabelCleared: - title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - } - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title + "\r\n\r\n" + text, - //Markdown: "# " + title + "\n" + text, - Title: title, - HideAvatar: "0", - SingleTitle: "view pull request", - SingleURL: p.PullRequest.HTMLURL, - }, - }, nil -} - -func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event HookEventType) (*DingtalkPayload, error) { - var text, title string - switch p.Action { - case api.HookIssueSynchronized: - action, err := parseHookPullRequestEventType(event) - if err != nil { - return nil, err - } - - title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.Review.Content - - } - - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title + "\r\n\r\n" + text, - Title: title, - HideAvatar: "0", - SingleTitle: "view pull request", - SingleURL: p.PullRequest.HTMLURL, - }, - }, nil -} - -func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) { - var title, url string - switch p.Action { - case api.HookRepoCreated: - title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) - url = p.Repository.HTMLURL - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title, - Title: title, - HideAvatar: "0", - SingleTitle: "view repository", - SingleURL: url, - }, - }, nil - case api.HookRepoDeleted: - title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - return &DingtalkPayload{ - MsgType: "text", - Text: struct { - Content string `json:"content"` - }{ - Content: title, - }, - }, nil - } - - 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 release", - SingleURL: url, - }, - }, nil - case api.HookReleaseUpdated: - title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) - url = p.Release.URL - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title, - Title: title, - HideAvatar: "0", - SingleTitle: "view release", - SingleURL: url, - }, - }, nil - - case api.HookReleaseDeleted: - title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) - url = p.Release.URL - return &DingtalkPayload{ - MsgType: "actionCard", - ActionCard: dingtalk.ActionCard{ - Text: title, - Title: title, - HideAvatar: "0", - SingleTitle: "view release", - 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) - - 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 HookEventPullRequestApproved, HookEventPullRequestRejected, HookEventPullRequestComment: - return getDingtalkPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - 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 deleted file mode 100644 index 32039edc9d..0000000000 --- a/models/webhook_discord.go +++ /dev/null @@ -1,585 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "encoding/json" - "errors" - "fmt" - "strconv" - "strings" - - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" -) - -type ( - // DiscordEmbedFooter for Embed Footer Structure. - DiscordEmbedFooter struct { - Text string `json:"text"` - } - - // DiscordEmbedAuthor for Embed Author Structure - DiscordEmbedAuthor struct { - Name string `json:"name"` - URL string `json:"url"` - IconURL string `json:"icon_url"` - } - - // DiscordEmbedField for Embed Field Structure - DiscordEmbedField struct { - Name string `json:"name"` - Value string `json:"value"` - } - - // DiscordEmbed is for Embed Structure - DiscordEmbed struct { - Title string `json:"title"` - Description string `json:"description"` - URL string `json:"url"` - Color int `json:"color"` - Footer DiscordEmbedFooter `json:"footer"` - Author DiscordEmbedAuthor `json:"author"` - Fields []DiscordEmbedField `json:"fields"` - } - - // DiscordPayload represents - DiscordPayload struct { - Wait bool `json:"wait"` - Content string `json:"content"` - Username string `json:"username"` - AvatarURL string `json:"avatar_url"` - TTS bool `json:"tts"` - Embeds []DiscordEmbed `json:"embeds"` - } - - // DiscordMeta contains the discord metadata - DiscordMeta struct { - Username string `json:"username"` - IconURL string `json:"icon_url"` - } -) - -func color(clr string) int { - if clr != "" { - clr = strings.TrimLeft(clr, "#") - if s, err := strconv.ParseInt(clr, 16, 32); err == nil { - return int(s) - } - } - - return 0 -} - -var ( - greenColor = color("1ac600") - greenColorLight = color("bfe5bf") - yellowColor = color("ffd930") - greyColor = color("4f545c") - purpleColor = color("7289da") - orangeColor = color("eb6420") - orangeColorLight = color("e68d60") - redColor = color("ff3232") -) - -// SetSecret sets the discord secret -func (p *DiscordPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the DiscordPayload to json -func (p *DiscordPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) { - // created tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf("[%s] %s %s created", 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: greenColor, - Author: DiscordEmbedAuthor{ - Name: p.Sender.UserName, - URL: setting.AppURL + p.Sender.UserName, - IconURL: p.Sender.AvatarURL, - }, - }, - }, - }, 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: redColor, - 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: greenColor, - 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) - commitDesc string - ) - - var titleLink string - if len(p.Commits) == 1 { - commitDesc = "1 new commit" - titleLink = p.Commits[0].URL - } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) - titleLink = p.CompareURL - } - if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName - } - - title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) - - var text string - // for each commit, generate attachment text - for i, commit := range p.Commits { - text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) - // add linebreak to each commit but the last - if i < len(p.Commits)-1 { - text += "\n" - } - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - Description: text, - URL: titleLink, - Color: greenColor, - Author: DiscordEmbedAuthor{ - Name: p.Sender.UserName, - URL: setting.AppURL + p.Sender.UserName, - IconURL: p.Sender.AvatarURL, - }, - }, - }, - }, nil -} - -func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) { - var text, title string - var color int - url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) - 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 = orangeColor - case api.HookIssueClosed: - title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - color = redColor - 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 = yellowColor - case api.HookIssueEdited: - title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - 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 = greenColor - case api.HookIssueUnassigned: - title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - 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 = yellowColor - 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 = yellowColor - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - Description: text, - URL: 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: - if p.IsPull { - title = "New comment on pull request " + title - color = greenColorLight - } else { - title = "New comment on issue " + title - color = orangeColorLight - } - content = p.Comment.Body - case api.HookIssueCommentEdited: - if p.IsPull { - title = "Comment edited on pull request " + title - } else { - title = "Comment edited on issue " + title - } - content = p.Comment.Body - color = yellowColor - case api.HookIssueCommentDeleted: - if p.IsPull { - title = "Comment deleted on pull request " + title - } else { - title = "Comment deleted on issue " + title - } - url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) - content = p.Comment.Body - color = redColor - } - - title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) - - 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 - switch p.Action { - case api.HookIssueOpened: - title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = greenColor - case api.HookIssueClosed: - if p.PullRequest.HasMerged { - title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = purpleColor - } else { - title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = redColor - } - text = p.PullRequest.Body - case api.HookIssueReOpened: - title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueEdited: - title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueAssigned: - list := make([]string, len(p.PullRequest.Assignees)) - for i, user := range p.PullRequest.Assignees { - list[i] = user.UserName - } - title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName, - strings.Join(list, ", "), - p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = greenColor - case api.HookIssueUnassigned: - title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueLabelUpdated: - title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueLabelCleared: - title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - Description: text, - URL: p.PullRequest.HTMLURL, - Color: color, - Author: DiscordEmbedAuthor{ - Name: p.Sender.UserName, - URL: setting.AppURL + p.Sender.UserName, - IconURL: p.Sender.AvatarURL, - }, - }, - }, - }, nil -} - -func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event HookEventType) (*DiscordPayload, error) { - var text, title string - var color int - switch p.Action { - case api.HookIssueSynchronized: - action, err := parseHookPullRequestEventType(event) - if err != nil { - return nil, err - } - - title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.Review.Content - - switch event { - case HookEventPullRequestApproved: - color = greenColor - case HookEventPullRequestRejected: - color = redColor - case HookEventPullRequestComment: - color = greyColor - default: - color = yellowColor - } - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - Description: text, - URL: p.PullRequest.HTMLURL, - Color: color, - Author: DiscordEmbedAuthor{ - Name: p.Sender.UserName, - URL: setting.AppURL + p.Sender.UserName, - IconURL: p.Sender.AvatarURL, - }, - }, - }, - }, nil -} - -func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) { - var title, url string - var color int - switch p.Action { - case api.HookRepoCreated: - title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) - url = p.Repository.HTMLURL - color = greenColor - case api.HookRepoDeleted: - title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - color = redColor - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - URL: url, - Color: color, - Author: DiscordEmbedAuthor{ - Name: p.Sender.UserName, - URL: setting.AppURL + p.Sender.UserName, - IconURL: p.Sender.AvatarURL, - }, - }, - }, - }, 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 = greenColor - case api.HookReleaseUpdated: - title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) - url = p.Release.URL - color = yellowColor - case api.HookReleaseDeleted: - title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) - url = p.Release.URL - color = redColor - } - - return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, - Embeds: []DiscordEmbed{ - { - Title: title, - Description: 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) - - discord := &DiscordMeta{} - if err := json.Unmarshal([]byte(meta), &discord); err != nil { - return s, errors.New("GetDiscordPayload meta json:" + err.Error()) - } - - 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 HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment: - return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event) - case HookEventRepository: - return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) - case HookEventRelease: - return getDiscordReleasePayload(p.(*api.ReleasePayload), discord) - } - - return s, nil -} - -func parseHookPullRequestEventType(event HookEventType) (string, error) { - - switch event { - - case HookEventPullRequestApproved: - return "approved", nil - case HookEventPullRequestRejected: - return "rejected", nil - case HookEventPullRequestComment: - return "comment", nil - - default: - return "", errors.New("unknown event type") - } -} diff --git a/models/webhook_msteams.go b/models/webhook_msteams.go deleted file mode 100644 index e8cdcca3ca..0000000000 --- a/models/webhook_msteams.go +++ /dev/null @@ -1,729 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "encoding/json" - "fmt" - "strings" - - "code.gitea.io/gitea/modules/git" - api "code.gitea.io/gitea/modules/structs" -) - -type ( - // MSTeamsFact for Fact Structure - MSTeamsFact struct { - Name string `json:"name"` - Value string `json:"value"` - } - - // MSTeamsSection is a MessageCard section - MSTeamsSection struct { - ActivityTitle string `json:"activityTitle"` - ActivitySubtitle string `json:"activitySubtitle"` - ActivityImage string `json:"activityImage"` - Facts []MSTeamsFact `json:"facts"` - Text string `json:"text"` - } - - // MSTeamsAction is an action (creates buttons, links etc) - MSTeamsAction struct { - Type string `json:"@type"` - Name string `json:"name"` - Targets []MSTeamsActionTarget `json:"targets,omitempty"` - } - - // MSTeamsActionTarget is the actual link to follow, etc - MSTeamsActionTarget struct { - Os string `json:"os"` - URI string `json:"uri"` - } - - // MSTeamsPayload is the parent object - MSTeamsPayload struct { - Type string `json:"@type"` - Context string `json:"@context"` - ThemeColor string `json:"themeColor"` - Title string `json:"title"` - Summary string `json:"summary"` - Sections []MSTeamsSection `json:"sections"` - PotentialAction []MSTeamsAction `json:"potentialAction"` - } -) - -// SetSecret sets the MSTeams secret -func (p *MSTeamsPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the MSTeamsPayload to json -func (p *MSTeamsPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func getMSTeamsCreatePayload(p *api.CreatePayload) (*MSTeamsPayload, error) { - // created tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", greenColor), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repo.FullName, - }, - { - Name: fmt.Sprintf("%s:", p.RefType), - Value: refName, - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: p.Repo.HTMLURL + "/src/" + refName, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsDeletePayload(p *api.DeletePayload) (*MSTeamsPayload, error) { - // deleted tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", yellowColor), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repo.FullName, - }, - { - Name: fmt.Sprintf("%s:", p.RefType), - Value: refName, - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: p.Repo.HTMLURL + "/src/" + refName, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsForkPayload(p *api.ForkPayload) (*MSTeamsPayload, error) { - // fork - title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", greenColor), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Facts: []MSTeamsFact{ - { - Name: "Forkee:", - Value: p.Forkee.FullName, - }, - { - Name: "Repository:", - Value: p.Repo.FullName, - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: p.Repo.HTMLURL, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsPushPayload(p *api.PushPayload) (*MSTeamsPayload, error) { - var ( - branchName = git.RefEndName(p.Ref) - commitDesc string - ) - - var titleLink string - if len(p.Commits) == 1 { - commitDesc = "1 new commit" - titleLink = p.Commits[0].URL - } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) - titleLink = p.CompareURL - } - if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName - } - - title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) - - var text string - // for each commit, generate attachment text - for i, commit := range p.Commits { - text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) - // add linebreak to each commit but the last - if i < len(p.Commits)-1 { - text += "\n" - } - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", greenColor), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: text, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repo.FullName, - }, - { - Name: "Commit count:", - Value: fmt.Sprintf("%d", len(p.Commits)), - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: titleLink, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { - var text, title string - var color int - url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) - 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 = orangeColor - case api.HookIssueClosed: - title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - color = redColor - 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 = yellowColor - case api.HookIssueEdited: - title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - 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 = greenColor - case api.HookIssueUnassigned: - title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - 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 = yellowColor - 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 = yellowColor - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - text = p.Issue.Body - color = yellowColor - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: text, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - { - Name: "Issue #:", - Value: fmt.Sprintf("%d", p.Issue.ID), - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: url, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsIssueCommentPayload(p *api.IssueCommentPayload) (*MSTeamsPayload, 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: - if p.IsPull { - title = "New comment on pull request " + title - color = greenColorLight - } else { - title = "New comment on issue " + title - color = orangeColorLight - } - content = p.Comment.Body - case api.HookIssueCommentEdited: - if p.IsPull { - title = "Comment edited on pull request " + title - } else { - title = "Comment edited on issue " + title - } - content = p.Comment.Body - color = yellowColor - case api.HookIssueCommentDeleted: - if p.IsPull { - title = "Comment deleted on pull request " + title - } else { - title = "Comment deleted on issue " + title - } - url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) - content = p.Comment.Body - color = redColor - } - - title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: content, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - { - Name: "Issue #:", - Value: fmt.Sprintf("%d", p.Issue.ID), - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: url, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, error) { - var text, title string - var color int - switch p.Action { - case api.HookIssueOpened: - title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = greenColor - case api.HookIssueClosed: - if p.PullRequest.HasMerged { - title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = purpleColor - } else { - title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = redColor - } - text = p.PullRequest.Body - case api.HookIssueReOpened: - title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueEdited: - title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueAssigned: - list := make([]string, len(p.PullRequest.Assignees)) - for i, user := range p.PullRequest.Assignees { - list[i] = user.UserName - } - title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName, - strings.Join(list, ", "), - p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = greenColor - case api.HookIssueUnassigned: - title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueLabelUpdated: - title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueLabelCleared: - title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueSynchronized: - title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueMilestoned: - title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - case api.HookIssueDemilestoned: - title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = yellowColor - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: text, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - { - Name: "Pull request #:", - Value: fmt.Sprintf("%d", p.PullRequest.ID), - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: p.PullRequest.HTMLURL, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsPullRequestApprovalPayload(p *api.PullRequestPayload, event HookEventType) (*MSTeamsPayload, error) { - var text, title string - var color int - switch p.Action { - case api.HookIssueSynchronized: - action, err := parseHookPullRequestEventType(event) - if err != nil { - return nil, err - } - - title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.Review.Content - - switch event { - case HookEventPullRequestApproved: - color = greenColor - case HookEventPullRequestRejected: - color = redColor - case HookEventPullRequestComment: - color = greyColor - default: - color = yellowColor - } - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: text, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - { - Name: "Pull request #:", - Value: fmt.Sprintf("%d", p.PullRequest.ID), - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: p.PullRequest.HTMLURL, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsRepositoryPayload(p *api.RepositoryPayload) (*MSTeamsPayload, error) { - var title, url string - var color int - switch p.Action { - case api.HookRepoCreated: - title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) - url = p.Repository.HTMLURL - color = greenColor - case api.HookRepoDeleted: - title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - color = yellowColor - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: url, - }, - }, - }, - }, - }, nil -} - -func getMSTeamsReleasePayload(p *api.ReleasePayload) (*MSTeamsPayload, 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 = greenColor - case api.HookReleaseUpdated: - title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) - url = p.Release.URL - color = greenColor - case api.HookReleaseDeleted: - title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) - url = p.Release.URL - color = greenColor - } - - return &MSTeamsPayload{ - Type: "MessageCard", - Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", color), - Title: title, - Summary: title, - Sections: []MSTeamsSection{ - { - ActivityTitle: p.Sender.FullName, - ActivitySubtitle: p.Sender.UserName, - ActivityImage: p.Sender.AvatarURL, - Text: p.Release.Note, - Facts: []MSTeamsFact{ - { - Name: "Repository:", - Value: p.Repository.FullName, - }, - { - Name: "Tag:", - Value: p.Release.TagName, - }, - }, - }, - }, - PotentialAction: []MSTeamsAction{ - { - Type: "OpenUri", - Name: "View in Gitea", - Targets: []MSTeamsActionTarget{ - { - Os: "default", - URI: url, - }, - }, - }, - }, - }, nil -} - -// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event HookEventType, meta string) (*MSTeamsPayload, error) { - s := new(MSTeamsPayload) - - switch event { - case HookEventCreate: - return getMSTeamsCreatePayload(p.(*api.CreatePayload)) - case HookEventDelete: - return getMSTeamsDeletePayload(p.(*api.DeletePayload)) - case HookEventFork: - return getMSTeamsForkPayload(p.(*api.ForkPayload)) - case HookEventIssues: - return getMSTeamsIssuesPayload(p.(*api.IssuePayload)) - case HookEventIssueComment: - return getMSTeamsIssueCommentPayload(p.(*api.IssueCommentPayload)) - case HookEventPush: - return getMSTeamsPushPayload(p.(*api.PushPayload)) - case HookEventPullRequest: - return getMSTeamsPullRequestPayload(p.(*api.PullRequestPayload)) - case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment: - return getMSTeamsPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - case HookEventRepository: - return getMSTeamsRepositoryPayload(p.(*api.RepositoryPayload)) - case HookEventRelease: - return getMSTeamsReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil -} diff --git a/models/webhook_slack.go b/models/webhook_slack.go deleted file mode 100644 index 9c179bb24a..0000000000 --- a/models/webhook_slack.go +++ /dev/null @@ -1,423 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" -) - -// SlackMeta contains the slack metadata -type SlackMeta struct { - Channel string `json:"channel"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - Color string `json:"color"` -} - -// SlackPayload contains the information about the slack channel -type SlackPayload struct { - Channel string `json:"channel"` - Text string `json:"text"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - UnfurlLinks int `json:"unfurl_links"` - LinkNames int `json:"link_names"` - Attachments []SlackAttachment `json:"attachments"` -} - -// SlackAttachment contains the slack message -type SlackAttachment struct { - Fallback string `json:"fallback"` - Color string `json:"color"` - Title string `json:"title"` - Text string `json:"text"` -} - -// SetSecret sets the slack secret -func (p *SlackPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the SlackPayload to json -func (p *SlackPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -// SlackTextFormatter replaces &, <, > with HTML characters -// see: https://api.slack.com/docs/formatting -func SlackTextFormatter(s string) string { - // replace & < > - s = strings.Replace(s, "&", "&", -1) - s = strings.Replace(s, "<", "<", -1) - s = strings.Replace(s, ">", ">", -1) - return s -} - -// SlackShortTextFormatter replaces &, <, > with HTML characters -func SlackShortTextFormatter(s string) string { - s = strings.Split(s, "\n")[0] - // replace & < > - s = strings.Replace(s, "&", "&", -1) - s = strings.Replace(s, "<", "<", -1) - s = strings.Replace(s, ">", ">", -1) - return s -} - -// SlackLinkFormatter creates a link compatible with slack -func SlackLinkFormatter(url string, text string) string { - return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text)) -} - -// SlackLinkToRef slack-formatter link to a repo ref -func SlackLinkToRef(repoURL, ref string) string { - refName := git.RefEndName(ref) - switch { - case strings.HasPrefix(ref, git.BranchPrefix): - return SlackLinkFormatter(repoURL+"/src/branch/"+refName, refName) - case strings.HasPrefix(ref, git.TagPrefix): - return SlackLinkFormatter(repoURL+"/src/tag/"+refName, refName) - default: - return SlackLinkFormatter(repoURL+"/src/commit/"+refName, refName) - } -} - -func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) { - repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) - refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) - text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) - - return &SlackPayload{ - Channel: slack.Channel, - Text: text, - Username: slack.Username, - IconURL: slack.IconURL, - }, 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.Forkee.HTMLURL, p.Forkee.FullName) - forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.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) - case api.HookIssueMilestoned: - text = fmt.Sprintf("[%s] Issue milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueDemilestoned: - text = fmt.Sprintf("[%s] Issue milestone cleared: #%s %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) - var text string - - switch p.Action { - case api.HookReleasePublished: - text = fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName) - case api.HookReleaseUpdated: - text = fmt.Sprintf("[%s] new release %s updated by %s", repoLink, refLink, p.Sender.UserName) - case api.HookReleaseDeleted: - text = fmt.Sprintf("[%s] new release %s deleted 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 ( - commitDesc string - commitString string - ) - - if len(p.Commits) == 1 { - commitDesc = "1 new commit" - } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) - } - if len(p.CompareURL) > 0 { - commitString = SlackLinkFormatter(p.CompareURL, commitDesc) - } else { - commitString = commitDesc - } - - repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) - branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) - text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName) - - var attachmentText string - // for each commit, generate attachment text - for i, commit := range p.Commits { - attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) - // add linebreak to each commit but the last - if i < len(p.Commits)-1 { - attachmentText += "\n" - } - } - - return &SlackPayload{ - Channel: slack.Channel, - Text: text, - Username: slack.Username, - IconURL: slack.IconURL, - Attachments: []SlackAttachment{{ - Color: slack.Color, - Text: attachmentText, - }}, - }, nil -} - -func getSlackPullRequestPayload(p *api.PullRequestPayload, 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.PullRequest.Title)) - var text, title, attachmentText string - switch p.Action { - case api.HookIssueOpened: - text = fmt.Sprintf("[%s] Pull request submitted by %s", p.Repository.FullName, senderLink) - title = titleLink - attachmentText = SlackTextFormatter(p.PullRequest.Body) - case api.HookIssueClosed: - if p.PullRequest.HasMerged { - text = fmt.Sprintf("[%s] Pull request merged: %s by %s", p.Repository.FullName, titleLink, senderLink) - } else { - text = fmt.Sprintf("[%s] Pull request closed: %s by %s", p.Repository.FullName, titleLink, senderLink) - } - case api.HookIssueReOpened: - text = fmt.Sprintf("[%s] Pull request re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueEdited: - text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink) - attachmentText = SlackTextFormatter(p.PullRequest.Body) - case api.HookIssueAssigned: - list := make([]string, len(p.PullRequest.Assignees)) - for i, user := range p.PullRequest.Assignees { - list[i] = SlackLinkFormatter(setting.AppURL+user.UserName, user.UserName) - } - text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName, - strings.Join(list, ", "), - titleLink, senderLink) - case api.HookIssueUnassigned: - text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueLabelUpdated: - text = fmt.Sprintf("[%s] Pull request labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueLabelCleared: - text = fmt.Sprintf("[%s] Pull request labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueSynchronized: - text = fmt.Sprintf("[%s] Pull request synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueMilestoned: - text = fmt.Sprintf("[%s] Pull request milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink) - case api.HookIssueDemilestoned: - text = fmt.Sprintf("[%s] Pull request milestone cleared: #%s %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 getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackMeta, event HookEventType) (*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.PullRequest.Title)) - var text, title, attachmentText string - switch p.Action { - case api.HookIssueSynchronized: - action, err := parseHookPullRequestEventType(event) - if err != nil { - return nil, err - } - - text = fmt.Sprintf("[%s] Pull request review %s : %s by %s", p.Repository.FullName, action, 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 getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) { - senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) - var text, title, attachmentText string - switch p.Action { - case api.HookRepoCreated: - text = fmt.Sprintf("[%s] Repository created by %s", p.Repository.FullName, senderLink) - title = p.Repository.HTMLURL - case api.HookRepoDeleted: - text = fmt.Sprintf("[%s] Repository deleted by %s", p.Repository.FullName, senderLink) - } - - return &SlackPayload{ - Channel: slack.Channel, - Text: text, - Username: slack.Username, - IconURL: slack.IconURL, - Attachments: []SlackAttachment{{ - Color: slack.Color, - Title: title, - Text: attachmentText, - }}, - }, nil -} - -// GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) { - s := new(SlackPayload) - - slack := &SlackMeta{} - if err := json.Unmarshal([]byte(meta), &slack); err != nil { - return s, errors.New("GetSlackPayload meta json:" + err.Error()) - } - - 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 HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment: - return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event) - case HookEventRepository: - return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) - case HookEventRelease: - return getSlackReleasePayload(p.(*api.ReleasePayload), slack) - } - - return s, nil -} diff --git a/models/webhook_telegram.go b/models/webhook_telegram.go deleted file mode 100644 index ead669dd08..0000000000 --- a/models/webhook_telegram.go +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "encoding/json" - "fmt" - "html" - "strings" - - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/markup" - api "code.gitea.io/gitea/modules/structs" -) - -type ( - // TelegramPayload represents - TelegramPayload struct { - Message string `json:"text"` - ParseMode string `json:"parse_mode"` - DisableWebPreview bool `json:"disable_web_page_preview"` - } - - // TelegramMeta contains the telegram metadata - TelegramMeta struct { - BotToken string `json:"bot_token"` - ChatID string `json:"chat_id"` - } -) - -// SetSecret sets the telegram secret -func (p *TelegramPayload) SetSecret(_ string) {} - -// JSONPayload Marshals the TelegramPayload to json -func (p *TelegramPayload) JSONPayload() ([]byte, error) { - p.ParseMode = "HTML" - p.DisableWebPreview = true - p.Message = markup.Sanitize(p.Message) - data, err := json.MarshalIndent(p, "", " ") - if err != nil { - return []byte{}, err - } - return data, nil -} - -func getTelegramCreatePayload(p *api.CreatePayload) (*TelegramPayload, error) { - // created tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, - p.Repo.HTMLURL+"/src/"+refName, refName) - - return &TelegramPayload{ - Message: title, - }, nil -} - -func getTelegramDeletePayload(p *api.DeletePayload) (*TelegramPayload, error) { - // created tag/branch - refName := git.RefEndName(p.Ref) - title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, - p.Repo.HTMLURL+"/src/"+refName, refName) - - return &TelegramPayload{ - Message: title, - }, nil -} - -func getTelegramForkPayload(p *api.ForkPayload) (*TelegramPayload, error) { - title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName) - - return &TelegramPayload{ - Message: title, - }, nil -} - -func getTelegramPushPayload(p *api.PushPayload) (*TelegramPayload, error) { - var ( - branchName = git.RefEndName(p.Ref) - commitDesc string - ) - - var titleLink string - if len(p.Commits) == 1 { - commitDesc = "1 new commit" - titleLink = p.Commits[0].URL - } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) - titleLink = p.CompareURL - } - if titleLink == "" { - titleLink = p.Repo.HTMLURL + "/src/" + branchName - } - title := fmt.Sprintf(`[%s:%s] %s`, p.Repo.HTMLURL, p.Repo.FullName, titleLink, branchName, commitDesc) - - var text string - // for each commit, generate attachment text - for i, commit := range p.Commits { - var authorName string - if commit.Author != nil { - authorName = " - " + commit.Author.Name - } - text += fmt.Sprintf(`[%s] %s`, commit.URL, commit.ID[:7], - strings.TrimRight(commit.Message, "\r\n")) + authorName - // add linebreak to each commit but the last - if i < len(p.Commits)-1 { - text += "\n" - } - } - - return &TelegramPayload{ - Message: title + "\n" + text, - }, nil -} - -func getTelegramIssuesPayload(p *api.IssuePayload) (*TelegramPayload, error) { - var text, title string - switch p.Action { - case api.HookIssueOpened: - title = fmt.Sprintf(`[%s] Issue opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueClosed: - title = fmt.Sprintf(`[%s] Issue closed: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueReOpened: - title = fmt.Sprintf(`[%s] Issue re-opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueEdited: - title = fmt.Sprintf(`[%s] Issue edited: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueAssigned: - title = fmt.Sprintf(`[%s] Issue assigned to %s: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.Assignee.UserName, p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueUnassigned: - title = fmt.Sprintf(`[%s] Issue unassigned: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueLabelUpdated: - title = fmt.Sprintf(`[%s] Issue labels updated: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueLabelCleared: - title = fmt.Sprintf(`[%s] Issue labels cleared: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueSynchronized: - title = fmt.Sprintf(`[%s] Issue synchronized: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueMilestoned: - title = fmt.Sprintf(`[%s] Issue milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - case api.HookIssueDemilestoned: - title = fmt.Sprintf(`[%s] Issue clear milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.Issue.URL, p.Index, p.Issue.Title) - text = p.Issue.Body - } - - return &TelegramPayload{ - Message: title + "\n\n" + text, - }, nil -} - -func getTelegramIssueCommentPayload(p *api.IssueCommentPayload) (*TelegramPayload, error) { - url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) - title := fmt.Sprintf(`#%d %s`, url, p.Issue.Index, html.EscapeString(p.Issue.Title)) - var text string - switch p.Action { - case api.HookIssueCommentCreated: - text = "New comment: " + title - text += p.Comment.Body - case api.HookIssueCommentEdited: - text = "Comment edited: " + title - text += p.Comment.Body - case api.HookIssueCommentDeleted: - text = "Comment deleted: " + title - text += p.Comment.Body - } - - return &TelegramPayload{ - Message: title + "\n" + text, - }, nil -} - -func getTelegramPullRequestPayload(p *api.PullRequestPayload) (*TelegramPayload, error) { - var text, title string - switch p.Action { - case api.HookIssueOpened: - title = fmt.Sprintf(`[%s] Pull request opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueClosed: - if p.PullRequest.HasMerged { - title = fmt.Sprintf(`[%s] Pull request merged: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - } else { - title = fmt.Sprintf(`[%s] Pull request closed: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - } - text = p.PullRequest.Body - case api.HookIssueReOpened: - title = fmt.Sprintf(`[%s] Pull request re-opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueEdited: - title = fmt.Sprintf(`[%s] Pull request edited: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueAssigned: - list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID}) - if err != nil { - return &TelegramPayload{}, err - } - title = fmt.Sprintf(`[%s] Pull request assigned to %s: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - list, p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueUnassigned: - title = fmt.Sprintf(`[%s] Pull request unassigned: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueLabelUpdated: - title = fmt.Sprintf(`[%s] Pull request labels updated: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueLabelCleared: - title = fmt.Sprintf(`[%s] Pull request labels cleared: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueSynchronized: - title = fmt.Sprintf(`[%s] Pull request synchronized: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueMilestoned: - title = fmt.Sprintf(`[%s] Pull request milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - case api.HookIssueDemilestoned: - title = fmt.Sprintf(`[%s] Pull request clear milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, - p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - } - - return &TelegramPayload{ - Message: title + "\n" + text, - }, nil -} - -func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, error) { - var title string - switch p.Action { - case api.HookRepoCreated: - title = fmt.Sprintf(`[%s] Repository created`, p.Repository.HTMLURL, p.Repository.FullName) - return &TelegramPayload{ - Message: title, - }, nil - case api.HookRepoDeleted: - title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - return &TelegramPayload{ - Message: title, - }, nil - } - return nil, nil -} - -func getTelegramReleasePayload(p *api.ReleasePayload) (*TelegramPayload, 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 &TelegramPayload{ - Message: title + "\n" + url, - }, nil - case api.HookReleaseUpdated: - title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) - url = p.Release.URL - return &TelegramPayload{ - Message: title + "\n" + url, - }, nil - - case api.HookReleaseDeleted: - title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) - url = p.Release.URL - return &TelegramPayload{ - Message: title + "\n" + url, - }, nil - } - - return nil, nil -} - -// GetTelegramPayload converts a telegram webhook into a TelegramPayload -func GetTelegramPayload(p api.Payloader, event HookEventType, meta string) (*TelegramPayload, error) { - s := new(TelegramPayload) - - switch event { - case HookEventCreate: - return getTelegramCreatePayload(p.(*api.CreatePayload)) - case HookEventDelete: - return getTelegramDeletePayload(p.(*api.DeletePayload)) - case HookEventFork: - return getTelegramForkPayload(p.(*api.ForkPayload)) - case HookEventIssues: - return getTelegramIssuesPayload(p.(*api.IssuePayload)) - case HookEventIssueComment: - return getTelegramIssueCommentPayload(p.(*api.IssueCommentPayload)) - case HookEventPush: - return getTelegramPushPayload(p.(*api.PushPayload)) - case HookEventPullRequest: - return getTelegramPullRequestPayload(p.(*api.PullRequestPayload)) - case HookEventRepository: - return getTelegramRepositoryPayload(p.(*api.RepositoryPayload)) - case HookEventRelease: - return getTelegramReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil -} diff --git a/models/webhook_test.go b/models/webhook_test.go index 7bdaadc5ae..0fd9b245ca 100644 --- a/models/webhook_test.go +++ b/models/webhook_test.go @@ -24,18 +24,6 @@ func TestIsValidHookContentType(t *testing.T) { assert.False(t, IsValidHookContentType("invalid")) } -func TestWebhook_GetSlackHook(t *testing.T) { - w := &Webhook{ - Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, - } - slackHook := w.GetSlackHook() - assert.Equal(t, *slackHook, SlackMeta{ - Channel: "foo", - Username: "username", - Color: "blue", - }) -} - func TestWebhook_History(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) webhook := AssertExistsAndLoadBean(t, &Webhook{ID: 1}).(*Webhook) diff --git a/modules/webhook/dingtalk.go b/modules/webhook/dingtalk.go new file mode 100644 index 0000000000..b6d58f55cf --- /dev/null +++ b/modules/webhook/dingtalk.go @@ -0,0 +1,423 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "encoding/json" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" + + dingtalk "github.com/lunny/dingtalk_webhook" +) + +type ( + // DingtalkPayload represents + DingtalkPayload dingtalk.Payload +) + +// SetSecret sets the dingtalk secret +func (p *DingtalkPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the DingtalkPayload to json +func (p *DingtalkPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: fmt.Sprintf("view ref %s", refName), + SingleURL: p.Repo.HTMLURL + "/src/" + refName, + }, + }, 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 ref %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) + commitDesc string + ) + + var titleLink, linkText string + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + titleLink = p.Commits[0].URL + linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7]) + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + titleLink = p.CompareURL + linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7]) + } + if titleLink == "" { + titleLink = p.Repo.HTMLURL + "/src/" + branchName + } + + title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) + + var text string + // for each commit, generate attachment text + for i, commit := range p.Commits { + var authorName string + if commit.Author != nil { + authorName = " - " + commit.Author.Name + } + text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n")) + authorName + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + text += "\n" + } + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: text, + Title: title, + HideAvatar: "0", + SingleTitle: linkText, + SingleURL: titleLink, + }, + }, 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] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title + "\r\n\r\n" + text, + //Markdown: "# " + title + "\n" + text, + Title: title, + HideAvatar: "0", + SingleTitle: "view issue", + 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, models.CommentHashTag(p.Comment.ID)) + var content string + switch p.Action { + case api.HookIssueCommentCreated: + if p.IsPull { + title = "New comment on pull request " + title + } else { + title = "New comment on issue " + title + } + content = p.Comment.Body + case api.HookIssueCommentEdited: + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } + content = p.Comment.Body + case api.HookIssueCommentDeleted: + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + } + + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title + "\r\n\r\n" + content, + Title: title, + HideAvatar: "0", + SingleTitle: "view issue comment", + SingleURL: url, + }, + }, nil +} + +func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + } else { + title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + } + text = p.PullRequest.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueAssigned: + list := make([]string, len(p.PullRequest.Assignees)) + for i, user := range p.PullRequest.Assignees { + list[i] = user.UserName + } + title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName, + strings.Join(list, ", "), + p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title + "\r\n\r\n" + text, + //Markdown: "# " + title + "\n" + text, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: p.PullRequest.HTMLURL, + }, + }, nil +} + +func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*DingtalkPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) + text = p.Review.Content + + } + + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title + "\r\n\r\n" + text, + Title: title, + HideAvatar: "0", + SingleTitle: "view pull request", + SingleURL: p.PullRequest.HTMLURL, + }, + }, nil +} + +func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) { + var title, url string + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) + url = p.Repository.HTMLURL + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: "view repository", + SingleURL: url, + }, + }, nil + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + return &DingtalkPayload{ + MsgType: "text", + Text: struct { + Content string `json:"content"` + }{ + Content: title, + }, + }, nil + } + + 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 release", + SingleURL: url, + }, + }, nil + case api.HookReleaseUpdated: + title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) + url = p.Release.URL + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: "view release", + SingleURL: url, + }, + }, nil + + case api.HookReleaseDeleted: + title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) + url = p.Release.URL + return &DingtalkPayload{ + MsgType: "actionCard", + ActionCard: dingtalk.ActionCard{ + Text: title, + Title: title, + HideAvatar: "0", + SingleTitle: "view release", + SingleURL: url, + }, + }, nil + } + + return nil, nil +} + +// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload +func GetDingtalkPayload(p api.Payloader, event models.HookEventType, meta string) (*DingtalkPayload, error) { + s := new(DingtalkPayload) + + switch event { + case models.HookEventCreate: + return getDingtalkCreatePayload(p.(*api.CreatePayload)) + case models.HookEventDelete: + return getDingtalkDeletePayload(p.(*api.DeletePayload)) + case models.HookEventFork: + return getDingtalkForkPayload(p.(*api.ForkPayload)) + case models.HookEventIssues: + return getDingtalkIssuesPayload(p.(*api.IssuePayload)) + case models.HookEventIssueComment: + return getDingtalkIssueCommentPayload(p.(*api.IssueCommentPayload)) + case models.HookEventPush: + return getDingtalkPushPayload(p.(*api.PushPayload)) + case models.HookEventPullRequest: + return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) + case models.HookEventPullRequestApproved, models.HookEventPullRequestRejected, models.HookEventPullRequestComment: + return getDingtalkPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) + case models.HookEventRepository: + return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) + case models.HookEventRelease: + return getDingtalkReleasePayload(p.(*api.ReleasePayload)) + } + + return s, nil +} diff --git a/modules/webhook/discord.go b/modules/webhook/discord.go new file mode 100644 index 0000000000..f92157a1ab --- /dev/null +++ b/modules/webhook/discord.go @@ -0,0 +1,596 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" +) + +type ( + // DiscordEmbedFooter for Embed Footer Structure. + DiscordEmbedFooter struct { + Text string `json:"text"` + } + + // DiscordEmbedAuthor for Embed Author Structure + DiscordEmbedAuthor struct { + Name string `json:"name"` + URL string `json:"url"` + IconURL string `json:"icon_url"` + } + + // DiscordEmbedField for Embed Field Structure + DiscordEmbedField struct { + Name string `json:"name"` + Value string `json:"value"` + } + + // DiscordEmbed is for Embed Structure + DiscordEmbed struct { + Title string `json:"title"` + Description string `json:"description"` + URL string `json:"url"` + Color int `json:"color"` + Footer DiscordEmbedFooter `json:"footer"` + Author DiscordEmbedAuthor `json:"author"` + Fields []DiscordEmbedField `json:"fields"` + } + + // DiscordPayload represents + DiscordPayload struct { + Wait bool `json:"wait"` + Content string `json:"content"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + TTS bool `json:"tts"` + Embeds []DiscordEmbed `json:"embeds"` + } + + // DiscordMeta contains the discord metadata + DiscordMeta struct { + Username string `json:"username"` + IconURL string `json:"icon_url"` + } +) + +// GetDiscordHook returns discord metadata +func GetDiscordHook(w *models.Webhook) *DiscordMeta { + s := &DiscordMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err) + } + return s +} + +func color(clr string) int { + if clr != "" { + clr = strings.TrimLeft(clr, "#") + if s, err := strconv.ParseInt(clr, 16, 32); err == nil { + return int(s) + } + } + + return 0 +} + +var ( + greenColor = color("1ac600") + greenColorLight = color("bfe5bf") + yellowColor = color("ffd930") + greyColor = color("4f545c") + purpleColor = color("7289da") + orangeColor = color("eb6420") + orangeColorLight = color("e68d60") + redColor = color("ff3232") +) + +// SetSecret sets the discord secret +func (p *DiscordPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the DiscordPayload to json +func (p *DiscordPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s created", 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: greenColor, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, 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: redColor, + 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: greenColor, + 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) + commitDesc string + ) + + var titleLink string + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + titleLink = p.Commits[0].URL + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + titleLink = p.CompareURL + } + if titleLink == "" { + titleLink = p.Repo.HTMLURL + "/src/" + branchName + } + + title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) + + var text string + // for each commit, generate attachment text + for i, commit := range p.Commits { + text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + text += "\n" + } + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: titleLink, + Color: greenColor, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + +func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) { + var text, title string + var color int + url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + 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 = orangeColor + case api.HookIssueClosed: + title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + color = redColor + 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 = yellowColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + 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 = greenColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + 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 = yellowColor + 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 = yellowColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: 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, models.CommentHashTag(p.Comment.ID)) + content := "" + var color int + switch p.Action { + case api.HookIssueCommentCreated: + if p.IsPull { + title = "New comment on pull request " + title + color = greenColorLight + } else { + title = "New comment on issue " + title + color = orangeColorLight + } + content = p.Comment.Body + case api.HookIssueCommentEdited: + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } + content = p.Comment.Body + color = yellowColor + case api.HookIssueCommentDeleted: + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + color = redColor + } + + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + + 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 + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = greenColor + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + color = purpleColor + } else { + title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + color = redColor + } + text = p.PullRequest.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueAssigned: + list := make([]string, len(p.PullRequest.Assignees)) + for i, user := range p.PullRequest.Assignees { + list[i] = user.UserName + } + title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName, + strings.Join(list, ", "), + p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = greenColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: p.PullRequest.HTMLURL, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + +func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event models.HookEventType) (*DiscordPayload, error) { + var text, title string + var color int + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) + text = p.Review.Content + + switch event { + case models.HookEventPullRequestApproved: + color = greenColor + case models.HookEventPullRequestRejected: + color = redColor + case models.HookEventPullRequestComment: + color = greyColor + default: + color = yellowColor + } + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: text, + URL: p.PullRequest.HTMLURL, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + +func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) { + var title, url string + var color int + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) + url = p.Repository.HTMLURL + color = greenColor + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + color = redColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + URL: url, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, 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 = greenColor + case api.HookReleaseUpdated: + title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) + url = p.Release.URL + color = yellowColor + case api.HookReleaseDeleted: + title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) + url = p.Release.URL + color = redColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + Description: 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 models.HookEventType, meta string) (*DiscordPayload, error) { + s := new(DiscordPayload) + + discord := &DiscordMeta{} + if err := json.Unmarshal([]byte(meta), &discord); err != nil { + return s, errors.New("GetDiscordPayload meta json:" + err.Error()) + } + + switch event { + case models.HookEventCreate: + return getDiscordCreatePayload(p.(*api.CreatePayload), discord) + case models.HookEventDelete: + return getDiscordDeletePayload(p.(*api.DeletePayload), discord) + case models.HookEventFork: + return getDiscordForkPayload(p.(*api.ForkPayload), discord) + case models.HookEventIssues: + return getDiscordIssuesPayload(p.(*api.IssuePayload), discord) + case models.HookEventIssueComment: + return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord) + case models.HookEventPush: + return getDiscordPushPayload(p.(*api.PushPayload), discord) + case models.HookEventPullRequest: + return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) + case models.HookEventPullRequestRejected, models.HookEventPullRequestApproved, models.HookEventPullRequestComment: + return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event) + case models.HookEventRepository: + return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) + case models.HookEventRelease: + return getDiscordReleasePayload(p.(*api.ReleasePayload), discord) + } + + return s, nil +} + +func parseHookPullRequestEventType(event models.HookEventType) (string, error) { + + switch event { + + case models.HookEventPullRequestApproved: + return "approved", nil + case models.HookEventPullRequestRejected: + return "rejected", nil + case models.HookEventPullRequestComment: + return "comment", nil + + default: + return "", errors.New("unknown event type") + } +} diff --git a/modules/webhook/msteams.go b/modules/webhook/msteams.go new file mode 100644 index 0000000000..2636e29983 --- /dev/null +++ b/modules/webhook/msteams.go @@ -0,0 +1,730 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "encoding/json" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" +) + +type ( + // MSTeamsFact for Fact Structure + MSTeamsFact struct { + Name string `json:"name"` + Value string `json:"value"` + } + + // MSTeamsSection is a MessageCard section + MSTeamsSection struct { + ActivityTitle string `json:"activityTitle"` + ActivitySubtitle string `json:"activitySubtitle"` + ActivityImage string `json:"activityImage"` + Facts []MSTeamsFact `json:"facts"` + Text string `json:"text"` + } + + // MSTeamsAction is an action (creates buttons, links etc) + MSTeamsAction struct { + Type string `json:"@type"` + Name string `json:"name"` + Targets []MSTeamsActionTarget `json:"targets,omitempty"` + } + + // MSTeamsActionTarget is the actual link to follow, etc + MSTeamsActionTarget struct { + Os string `json:"os"` + URI string `json:"uri"` + } + + // MSTeamsPayload is the parent object + MSTeamsPayload struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor string `json:"themeColor"` + Title string `json:"title"` + Summary string `json:"summary"` + Sections []MSTeamsSection `json:"sections"` + PotentialAction []MSTeamsAction `json:"potentialAction"` + } +) + +// SetSecret sets the MSTeams secret +func (p *MSTeamsPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the MSTeamsPayload to json +func (p *MSTeamsPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +func getMSTeamsCreatePayload(p *api.CreatePayload) (*MSTeamsPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", greenColor), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repo.FullName, + }, + { + Name: fmt.Sprintf("%s:", p.RefType), + Value: refName, + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: p.Repo.HTMLURL + "/src/" + refName, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsDeletePayload(p *api.DeletePayload) (*MSTeamsPayload, error) { + // deleted tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", yellowColor), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repo.FullName, + }, + { + Name: fmt.Sprintf("%s:", p.RefType), + Value: refName, + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: p.Repo.HTMLURL + "/src/" + refName, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsForkPayload(p *api.ForkPayload) (*MSTeamsPayload, error) { + // fork + title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", greenColor), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Facts: []MSTeamsFact{ + { + Name: "Forkee:", + Value: p.Forkee.FullName, + }, + { + Name: "Repository:", + Value: p.Repo.FullName, + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: p.Repo.HTMLURL, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsPushPayload(p *api.PushPayload) (*MSTeamsPayload, error) { + var ( + branchName = git.RefEndName(p.Ref) + commitDesc string + ) + + var titleLink string + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + titleLink = p.Commits[0].URL + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + titleLink = p.CompareURL + } + if titleLink == "" { + titleLink = p.Repo.HTMLURL + "/src/" + branchName + } + + title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) + + var text string + // for each commit, generate attachment text + for i, commit := range p.Commits { + text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + text += "\n" + } + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", greenColor), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: text, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repo.FullName, + }, + { + Name: "Commit count:", + Value: fmt.Sprintf("%d", len(p.Commits)), + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: titleLink, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { + var text, title string + var color int + url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + 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 = orangeColor + case api.HookIssueClosed: + title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + color = redColor + 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 = yellowColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + 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 = greenColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + 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 = yellowColor + 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 = yellowColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = yellowColor + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: text, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + { + Name: "Issue #:", + Value: fmt.Sprintf("%d", p.Issue.ID), + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: url, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsIssueCommentPayload(p *api.IssueCommentPayload) (*MSTeamsPayload, 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, models.CommentHashTag(p.Comment.ID)) + content := "" + var color int + switch p.Action { + case api.HookIssueCommentCreated: + if p.IsPull { + title = "New comment on pull request " + title + color = greenColorLight + } else { + title = "New comment on issue " + title + color = orangeColorLight + } + content = p.Comment.Body + case api.HookIssueCommentEdited: + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } + content = p.Comment.Body + color = yellowColor + case api.HookIssueCommentDeleted: + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + color = redColor + } + + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: content, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + { + Name: "Issue #:", + Value: fmt.Sprintf("%d", p.Issue.ID), + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: url, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, error) { + var text, title string + var color int + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = greenColor + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + color = purpleColor + } else { + title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + color = redColor + } + text = p.PullRequest.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueAssigned: + list := make([]string, len(p.PullRequest.Assignees)) + for i, user := range p.PullRequest.Assignees { + list[i] = user.UserName + } + title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName, + strings.Join(list, ", "), + p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = greenColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + case api.HookIssueDemilestoned: + title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + color = yellowColor + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: text, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + { + Name: "Pull request #:", + Value: fmt.Sprintf("%d", p.PullRequest.ID), + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: p.PullRequest.HTMLURL, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*MSTeamsPayload, error) { + var text, title string + var color int + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) + text = p.Review.Content + + switch event { + case models.HookEventPullRequestApproved: + color = greenColor + case models.HookEventPullRequestRejected: + color = redColor + case models.HookEventPullRequestComment: + color = greyColor + default: + color = yellowColor + } + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: text, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + { + Name: "Pull request #:", + Value: fmt.Sprintf("%d", p.PullRequest.ID), + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: p.PullRequest.HTMLURL, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsRepositoryPayload(p *api.RepositoryPayload) (*MSTeamsPayload, error) { + var title, url string + var color int + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) + url = p.Repository.HTMLURL + color = greenColor + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + color = yellowColor + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: url, + }, + }, + }, + }, + }, nil +} + +func getMSTeamsReleasePayload(p *api.ReleasePayload) (*MSTeamsPayload, 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 = greenColor + case api.HookReleaseUpdated: + title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) + url = p.Release.URL + color = greenColor + case api.HookReleaseDeleted: + title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) + url = p.Release.URL + color = greenColor + } + + return &MSTeamsPayload{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: fmt.Sprintf("%x", color), + Title: title, + Summary: title, + Sections: []MSTeamsSection{ + { + ActivityTitle: p.Sender.FullName, + ActivitySubtitle: p.Sender.UserName, + ActivityImage: p.Sender.AvatarURL, + Text: p.Release.Note, + Facts: []MSTeamsFact{ + { + Name: "Repository:", + Value: p.Repository.FullName, + }, + { + Name: "Tag:", + Value: p.Release.TagName, + }, + }, + }, + }, + PotentialAction: []MSTeamsAction{ + { + Type: "OpenUri", + Name: "View in Gitea", + Targets: []MSTeamsActionTarget{ + { + Os: "default", + URI: url, + }, + }, + }, + }, + }, nil +} + +// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload +func GetMSTeamsPayload(p api.Payloader, event models.HookEventType, meta string) (*MSTeamsPayload, error) { + s := new(MSTeamsPayload) + + switch event { + case models.HookEventCreate: + return getMSTeamsCreatePayload(p.(*api.CreatePayload)) + case models.HookEventDelete: + return getMSTeamsDeletePayload(p.(*api.DeletePayload)) + case models.HookEventFork: + return getMSTeamsForkPayload(p.(*api.ForkPayload)) + case models.HookEventIssues: + return getMSTeamsIssuesPayload(p.(*api.IssuePayload)) + case models.HookEventIssueComment: + return getMSTeamsIssueCommentPayload(p.(*api.IssueCommentPayload)) + case models.HookEventPush: + return getMSTeamsPushPayload(p.(*api.PushPayload)) + case models.HookEventPullRequest: + return getMSTeamsPullRequestPayload(p.(*api.PullRequestPayload)) + case models.HookEventPullRequestRejected, models.HookEventPullRequestApproved, models.HookEventPullRequestComment: + return getMSTeamsPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) + case models.HookEventRepository: + return getMSTeamsRepositoryPayload(p.(*api.RepositoryPayload)) + case models.HookEventRelease: + return getMSTeamsReleasePayload(p.(*api.ReleasePayload)) + } + + return s, nil +} diff --git a/modules/webhook/slack.go b/modules/webhook/slack.go new file mode 100644 index 0000000000..7d844bfa50 --- /dev/null +++ b/modules/webhook/slack.go @@ -0,0 +1,434 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" +) + +// SlackMeta contains the slack metadata +type SlackMeta struct { + Channel string `json:"channel"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + Color string `json:"color"` +} + +// GetSlackHook returns slack metadata +func GetSlackHook(w *models.Webhook) *SlackMeta { + s := &SlackMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("webhook.GetSlackHook(%d): %v", w.ID, err) + } + return s +} + +// SlackPayload contains the information about the slack channel +type SlackPayload struct { + Channel string `json:"channel"` + Text string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + UnfurlLinks int `json:"unfurl_links"` + LinkNames int `json:"link_names"` + Attachments []SlackAttachment `json:"attachments"` +} + +// SlackAttachment contains the slack message +type SlackAttachment struct { + Fallback string `json:"fallback"` + Color string `json:"color"` + Title string `json:"title"` + Text string `json:"text"` +} + +// SetSecret sets the slack secret +func (p *SlackPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the SlackPayload to json +func (p *SlackPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +// SlackTextFormatter replaces &, <, > with HTML characters +// see: https://api.slack.com/docs/formatting +func SlackTextFormatter(s string) string { + // replace & < > + s = strings.Replace(s, "&", "&", -1) + s = strings.Replace(s, "<", "<", -1) + s = strings.Replace(s, ">", ">", -1) + return s +} + +// SlackShortTextFormatter replaces &, <, > with HTML characters +func SlackShortTextFormatter(s string) string { + s = strings.Split(s, "\n")[0] + // replace & < > + s = strings.Replace(s, "&", "&", -1) + s = strings.Replace(s, "<", "<", -1) + s = strings.Replace(s, ">", ">", -1) + return s +} + +// SlackLinkFormatter creates a link compatible with slack +func SlackLinkFormatter(url string, text string) string { + return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text)) +} + +// SlackLinkToRef slack-formatter link to a repo ref +func SlackLinkToRef(repoURL, ref string) string { + refName := git.RefEndName(ref) + switch { + case strings.HasPrefix(ref, git.BranchPrefix): + return SlackLinkFormatter(repoURL+"/src/branch/"+refName, refName) + case strings.HasPrefix(ref, git.TagPrefix): + return SlackLinkFormatter(repoURL+"/src/tag/"+refName, refName) + default: + return SlackLinkFormatter(repoURL+"/src/commit/"+refName, refName) + } +} + +func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) { + repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) + text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + }, 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.Forkee.HTMLURL, p.Forkee.FullName) + forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.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) + case api.HookIssueMilestoned: + text = fmt.Sprintf("[%s] Issue milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueDemilestoned: + text = fmt.Sprintf("[%s] Issue milestone cleared: #%s %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, models.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) + var text string + + switch p.Action { + case api.HookReleasePublished: + text = fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName) + case api.HookReleaseUpdated: + text = fmt.Sprintf("[%s] new release %s updated by %s", repoLink, refLink, p.Sender.UserName) + case api.HookReleaseDeleted: + text = fmt.Sprintf("[%s] new release %s deleted 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 ( + commitDesc string + commitString string + ) + + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + } + if len(p.CompareURL) > 0 { + commitString = SlackLinkFormatter(p.CompareURL, commitDesc) + } else { + commitString = commitDesc + } + + repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) + text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName) + + var attachmentText string + // for each commit, generate attachment text + for i, commit := range p.Commits { + attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + attachmentText += "\n" + } + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Text: attachmentText, + }}, + }, nil +} + +func getSlackPullRequestPayload(p *api.PullRequestPayload, 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.PullRequest.Title)) + var text, title, attachmentText string + switch p.Action { + case api.HookIssueOpened: + text = fmt.Sprintf("[%s] Pull request submitted by %s", p.Repository.FullName, senderLink) + title = titleLink + attachmentText = SlackTextFormatter(p.PullRequest.Body) + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + text = fmt.Sprintf("[%s] Pull request merged: %s by %s", p.Repository.FullName, titleLink, senderLink) + } else { + text = fmt.Sprintf("[%s] Pull request closed: %s by %s", p.Repository.FullName, titleLink, senderLink) + } + case api.HookIssueReOpened: + text = fmt.Sprintf("[%s] Pull request re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueEdited: + text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink) + attachmentText = SlackTextFormatter(p.PullRequest.Body) + case api.HookIssueAssigned: + list := make([]string, len(p.PullRequest.Assignees)) + for i, user := range p.PullRequest.Assignees { + list[i] = SlackLinkFormatter(setting.AppURL+user.UserName, user.UserName) + } + text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName, + strings.Join(list, ", "), + titleLink, senderLink) + case api.HookIssueUnassigned: + text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueLabelUpdated: + text = fmt.Sprintf("[%s] Pull request labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueLabelCleared: + text = fmt.Sprintf("[%s] Pull request labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueSynchronized: + text = fmt.Sprintf("[%s] Pull request synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueMilestoned: + text = fmt.Sprintf("[%s] Pull request milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink) + case api.HookIssueDemilestoned: + text = fmt.Sprintf("[%s] Pull request milestone cleared: #%s %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 getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackMeta, event models.HookEventType) (*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.PullRequest.Title)) + var text, title, attachmentText string + switch p.Action { + case api.HookIssueSynchronized: + action, err := parseHookPullRequestEventType(event) + if err != nil { + return nil, err + } + + text = fmt.Sprintf("[%s] Pull request review %s : %s by %s", p.Repository.FullName, action, 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 getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) { + senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + var text, title, attachmentText string + switch p.Action { + case api.HookRepoCreated: + text = fmt.Sprintf("[%s] Repository created by %s", p.Repository.FullName, senderLink) + title = p.Repository.HTMLURL + case api.HookRepoDeleted: + text = fmt.Sprintf("[%s] Repository deleted by %s", p.Repository.FullName, senderLink) + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Title: title, + Text: attachmentText, + }}, + }, nil +} + +// GetSlackPayload converts a slack webhook into a SlackPayload +func GetSlackPayload(p api.Payloader, event models.HookEventType, meta string) (*SlackPayload, error) { + s := new(SlackPayload) + + slack := &SlackMeta{} + if err := json.Unmarshal([]byte(meta), &slack); err != nil { + return s, errors.New("GetSlackPayload meta json:" + err.Error()) + } + + switch event { + case models.HookEventCreate: + return getSlackCreatePayload(p.(*api.CreatePayload), slack) + case models.HookEventDelete: + return getSlackDeletePayload(p.(*api.DeletePayload), slack) + case models.HookEventFork: + return getSlackForkPayload(p.(*api.ForkPayload), slack) + case models.HookEventIssues: + return getSlackIssuesPayload(p.(*api.IssuePayload), slack) + case models.HookEventIssueComment: + return getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack) + case models.HookEventPush: + return getSlackPushPayload(p.(*api.PushPayload), slack) + case models.HookEventPullRequest: + return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) + case models.HookEventPullRequestRejected, models.HookEventPullRequestApproved, models.HookEventPullRequestComment: + return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event) + case models.HookEventRepository: + return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) + case models.HookEventRelease: + return getSlackReleasePayload(p.(*api.ReleasePayload), slack) + } + + return s, nil +} diff --git a/modules/webhook/telegram.go b/modules/webhook/telegram.go new file mode 100644 index 0000000000..4e67b22954 --- /dev/null +++ b/modules/webhook/telegram.go @@ -0,0 +1,336 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "encoding/json" + "fmt" + "html" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + api "code.gitea.io/gitea/modules/structs" +) + +type ( + // TelegramPayload represents + TelegramPayload struct { + Message string `json:"text"` + ParseMode string `json:"parse_mode"` + DisableWebPreview bool `json:"disable_web_page_preview"` + } + + // TelegramMeta contains the telegram metadata + TelegramMeta struct { + BotToken string `json:"bot_token"` + ChatID string `json:"chat_id"` + } +) + +// GetTelegramHook returns telegram metadata +func GetTelegramHook(w *models.Webhook) *TelegramMeta { + s := &TelegramMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err) + } + return s +} + +// SetSecret sets the telegram secret +func (p *TelegramPayload) SetSecret(_ string) {} + +// JSONPayload Marshals the TelegramPayload to json +func (p *TelegramPayload) JSONPayload() ([]byte, error) { + p.ParseMode = "HTML" + p.DisableWebPreview = true + p.Message = markup.Sanitize(p.Message) + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return []byte{}, err + } + return data, nil +} + +func getTelegramCreatePayload(p *api.CreatePayload) (*TelegramPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, + p.Repo.HTMLURL+"/src/"+refName, refName) + + return &TelegramPayload{ + Message: title, + }, nil +} + +func getTelegramDeletePayload(p *api.DeletePayload) (*TelegramPayload, error) { + // created tag/branch + refName := git.RefEndName(p.Ref) + title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, + p.Repo.HTMLURL+"/src/"+refName, refName) + + return &TelegramPayload{ + Message: title, + }, nil +} + +func getTelegramForkPayload(p *api.ForkPayload) (*TelegramPayload, error) { + title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName) + + return &TelegramPayload{ + Message: title, + }, nil +} + +func getTelegramPushPayload(p *api.PushPayload) (*TelegramPayload, error) { + var ( + branchName = git.RefEndName(p.Ref) + commitDesc string + ) + + var titleLink string + if len(p.Commits) == 1 { + commitDesc = "1 new commit" + titleLink = p.Commits[0].URL + } else { + commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + titleLink = p.CompareURL + } + if titleLink == "" { + titleLink = p.Repo.HTMLURL + "/src/" + branchName + } + title := fmt.Sprintf(`[%s:%s] %s`, p.Repo.HTMLURL, p.Repo.FullName, titleLink, branchName, commitDesc) + + var text string + // for each commit, generate attachment text + for i, commit := range p.Commits { + var authorName string + if commit.Author != nil { + authorName = " - " + commit.Author.Name + } + text += fmt.Sprintf(`[%s] %s`, commit.URL, commit.ID[:7], + strings.TrimRight(commit.Message, "\r\n")) + authorName + // add linebreak to each commit but the last + if i < len(p.Commits)-1 { + text += "\n" + } + } + + return &TelegramPayload{ + Message: title + "\n" + text, + }, nil +} + +func getTelegramIssuesPayload(p *api.IssuePayload) (*TelegramPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf(`[%s] Issue opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueClosed: + title = fmt.Sprintf(`[%s] Issue closed: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueReOpened: + title = fmt.Sprintf(`[%s] Issue re-opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueEdited: + title = fmt.Sprintf(`[%s] Issue edited: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueAssigned: + title = fmt.Sprintf(`[%s] Issue assigned to %s: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.Assignee.UserName, p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueUnassigned: + title = fmt.Sprintf(`[%s] Issue unassigned: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueLabelUpdated: + title = fmt.Sprintf(`[%s] Issue labels updated: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf(`[%s] Issue labels cleared: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf(`[%s] Issue synchronized: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueMilestoned: + title = fmt.Sprintf(`[%s] Issue milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + case api.HookIssueDemilestoned: + title = fmt.Sprintf(`[%s] Issue clear milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.Issue.URL, p.Index, p.Issue.Title) + text = p.Issue.Body + } + + return &TelegramPayload{ + Message: title + "\n\n" + text, + }, nil +} + +func getTelegramIssueCommentPayload(p *api.IssueCommentPayload) (*TelegramPayload, error) { + url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, models.CommentHashTag(p.Comment.ID)) + title := fmt.Sprintf(`#%d %s`, url, p.Issue.Index, html.EscapeString(p.Issue.Title)) + var text string + switch p.Action { + case api.HookIssueCommentCreated: + text = "New comment: " + title + text += p.Comment.Body + case api.HookIssueCommentEdited: + text = "Comment edited: " + title + text += p.Comment.Body + case api.HookIssueCommentDeleted: + text = "Comment deleted: " + title + text += p.Comment.Body + } + + return &TelegramPayload{ + Message: title + "\n" + text, + }, nil +} + +func getTelegramPullRequestPayload(p *api.PullRequestPayload) (*TelegramPayload, error) { + var text, title string + switch p.Action { + case api.HookIssueOpened: + title = fmt.Sprintf(`[%s] Pull request opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueClosed: + if p.PullRequest.HasMerged { + title = fmt.Sprintf(`[%s] Pull request merged: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + } else { + title = fmt.Sprintf(`[%s] Pull request closed: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + } + text = p.PullRequest.Body + case api.HookIssueReOpened: + title = fmt.Sprintf(`[%s] Pull request re-opened: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueEdited: + title = fmt.Sprintf(`[%s] Pull request edited: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueAssigned: + list, err := models.MakeAssigneeList(&models.Issue{ID: p.PullRequest.ID}) + if err != nil { + return &TelegramPayload{}, err + } + title = fmt.Sprintf(`[%s] Pull request assigned to %s: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + list, p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueUnassigned: + title = fmt.Sprintf(`[%s] Pull request unassigned: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelUpdated: + title = fmt.Sprintf(`[%s] Pull request labels updated: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueLabelCleared: + title = fmt.Sprintf(`[%s] Pull request labels cleared: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueSynchronized: + title = fmt.Sprintf(`[%s] Pull request synchronized: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueMilestoned: + title = fmt.Sprintf(`[%s] Pull request milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + case api.HookIssueDemilestoned: + title = fmt.Sprintf(`[%s] Pull request clear milestone: #%d %s`, p.Repository.HTMLURL, p.Repository.FullName, + p.PullRequest.HTMLURL, p.Index, p.PullRequest.Title) + text = p.PullRequest.Body + } + + return &TelegramPayload{ + Message: title + "\n" + text, + }, nil +} + +func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, error) { + var title string + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf(`[%s] Repository created`, p.Repository.HTMLURL, p.Repository.FullName) + return &TelegramPayload{ + Message: title, + }, nil + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + return &TelegramPayload{ + Message: title, + }, nil + } + return nil, nil +} + +func getTelegramReleasePayload(p *api.ReleasePayload) (*TelegramPayload, 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 &TelegramPayload{ + Message: title + "\n" + url, + }, nil + case api.HookReleaseUpdated: + title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) + url = p.Release.URL + return &TelegramPayload{ + Message: title + "\n" + url, + }, nil + + case api.HookReleaseDeleted: + title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) + url = p.Release.URL + return &TelegramPayload{ + Message: title + "\n" + url, + }, nil + } + + return nil, nil +} + +// GetTelegramPayload converts a telegram webhook into a TelegramPayload +func GetTelegramPayload(p api.Payloader, event models.HookEventType, meta string) (*TelegramPayload, error) { + s := new(TelegramPayload) + + switch event { + case models.HookEventCreate: + return getTelegramCreatePayload(p.(*api.CreatePayload)) + case models.HookEventDelete: + return getTelegramDeletePayload(p.(*api.DeletePayload)) + case models.HookEventFork: + return getTelegramForkPayload(p.(*api.ForkPayload)) + case models.HookEventIssues: + return getTelegramIssuesPayload(p.(*api.IssuePayload)) + case models.HookEventIssueComment: + return getTelegramIssueCommentPayload(p.(*api.IssueCommentPayload)) + case models.HookEventPush: + return getTelegramPushPayload(p.(*api.PushPayload)) + case models.HookEventPullRequest: + return getTelegramPullRequestPayload(p.(*api.PullRequestPayload)) + case models.HookEventRepository: + return getTelegramRepositoryPayload(p.(*api.RepositoryPayload)) + case models.HookEventRelease: + return getTelegramReleasePayload(p.(*api.ReleasePayload)) + } + + return s, nil +} diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index 623a475df9..410e47461f 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -90,27 +90,27 @@ func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.Hoo // Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks. switch w.HookTaskType { case models.SLACK: - payloader, err = models.GetSlackPayload(p, event, w.Meta) + payloader, err = GetSlackPayload(p, event, w.Meta) if err != nil { return fmt.Errorf("GetSlackPayload: %v", err) } case models.DISCORD: - payloader, err = models.GetDiscordPayload(p, event, w.Meta) + payloader, err = GetDiscordPayload(p, event, w.Meta) if err != nil { return fmt.Errorf("GetDiscordPayload: %v", err) } case models.DINGTALK: - payloader, err = models.GetDingtalkPayload(p, event, w.Meta) + payloader, err = GetDingtalkPayload(p, event, w.Meta) if err != nil { return fmt.Errorf("GetDingtalkPayload: %v", err) } case models.TELEGRAM: - payloader, err = models.GetTelegramPayload(p, event, w.Meta) + payloader, err = GetTelegramPayload(p, event, w.Meta) if err != nil { return fmt.Errorf("GetTelegramPayload: %v", err) } case models.MSTEAMS: - payloader, err = models.GetMSTeamsPayload(p, event, w.Meta) + payloader, err = GetMSTeamsPayload(p, event, w.Meta) if err != nil { return fmt.Errorf("GetMSTeamsPayload: %v", err) } diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go index c944bc477d..e88e67e9bf 100644 --- a/modules/webhook/webhook_test.go +++ b/modules/webhook/webhook_test.go @@ -12,6 +12,18 @@ import ( "github.com/stretchr/testify/assert" ) +func TestWebhook_GetSlackHook(t *testing.T) { + w := &models.Webhook{ + Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, + } + slackHook := GetSlackHook(w) + assert.Equal(t, *slackHook, SlackMeta{ + Channel: "foo", + Username: "username", + Color: "blue", + }) +} + func TestPrepareWebhooks(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 07456f8dd6..6da53d6275 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/webhook" "github.com/unknwon/com" ) @@ -166,7 +167,7 @@ func ToHook(repoLink string, w *models.Webhook) *api.Hook { "content_type": w.ContentType.Name(), } if w.HookTaskType == models.SLACK { - s := w.GetSlackHook() + s := webhook.GetSlackHook(w) config["channel"] = s.Channel config["username"] = s.Username config["icon_url"] = s.IconURL diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 7903d58334..6f72e99b71 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/utils" @@ -129,7 +130,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID return nil, false } - meta, err := json.Marshal(&models.SlackMeta{ + meta, err := json.Marshal(&webhook.SlackMeta{ Channel: strings.TrimSpace(channel), Username: form.Config["username"], IconURL: form.Config["icon_url"], @@ -203,7 +204,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho if w.HookTaskType == models.SLACK { if channel, ok := form.Config["channel"]; ok { - meta, err := json.Marshal(&models.SlackMeta{ + meta, err := json.Marshal(&webhook.SlackMeta{ Channel: channel, Username: form.Config["username"], IconURL: form.Config["icon_url"], diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index a6bd3af264..9ae15882c1 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -268,7 +268,7 @@ func DiscordHooksNewPost(ctx *context.Context, form auth.NewDiscordHookForm) { return } - meta, err := json.Marshal(&models.DiscordMeta{ + meta, err := json.Marshal(&webhook.DiscordMeta{ Username: form.Username, IconURL: form.IconURL, }) @@ -357,7 +357,7 @@ func TelegramHooksNewPost(ctx *context.Context, form auth.NewTelegramHookForm) { return } - meta, err := json.Marshal(&models.TelegramMeta{ + meta, err := json.Marshal(&webhook.TelegramMeta{ BotToken: form.BotToken, ChatID: form.ChatID, }) @@ -452,7 +452,7 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) { return } - meta, err := json.Marshal(&models.SlackMeta{ + meta, err := json.Marshal(&webhook.SlackMeta{ Channel: strings.TrimSpace(form.Channel), Username: form.Username, IconURL: form.IconURL, @@ -515,11 +515,11 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) { ctx.Data["HookType"] = w.HookTaskType.Name() switch w.HookTaskType { case models.SLACK: - ctx.Data["SlackHook"] = w.GetSlackHook() + ctx.Data["SlackHook"] = webhook.GetSlackHook(w) case models.DISCORD: - ctx.Data["DiscordHook"] = w.GetDiscordHook() + ctx.Data["DiscordHook"] = webhook.GetDiscordHook(w) case models.TELEGRAM: - ctx.Data["TelegramHook"] = w.GetTelegramHook() + ctx.Data["TelegramHook"] = webhook.GetTelegramHook(w) } ctx.Data["History"], err = w.History(1) @@ -646,7 +646,7 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) { return } - meta, err := json.Marshal(&models.SlackMeta{ + meta, err := json.Marshal(&webhook.SlackMeta{ Channel: strings.TrimSpace(form.Channel), Username: form.Username, IconURL: form.IconURL, @@ -690,7 +690,7 @@ func DiscordHooksEditPost(ctx *context.Context, form auth.NewDiscordHookForm) { return } - meta, err := json.Marshal(&models.DiscordMeta{ + meta, err := json.Marshal(&webhook.DiscordMeta{ Username: form.Username, IconURL: form.IconURL, }) @@ -763,7 +763,7 @@ func TelegramHooksEditPost(ctx *context.Context, form auth.NewTelegramHookForm) ctx.HTML(200, orCtx.NewTemplate) return } - meta, err := json.Marshal(&models.TelegramMeta{ + meta, err := json.Marshal(&webhook.TelegramMeta{ BotToken: form.BotToken, ChatID: form.ChatID, })