From b9d1fb6de32613ada3869d2a9692cb078ed48534 Mon Sep 17 00:00:00 2001 From: Daniel Grier Date: Sat, 20 Apr 2019 00:18:06 +1000 Subject: [PATCH] Add support for MS Teams webhooks (#6632) --- docs/content/doc/features/comparison.en-us.md | 1 + models/webhook.go | 9 + models/webhook_msteams.go | 703 ++++++++++++++++++ modules/auth/repo_form.go | 11 + modules/setting/webhook.go | 2 +- options/locale/locale_en-US.ini | 1 + public/img/msteams.png | Bin 0 -> 6154 bytes routers/repo/webhook.go | 72 ++ routers/routes/routes.go | 4 + templates/admin/hook_new.tmpl | 3 + templates/org/settings/hook_new.tmpl | 3 + templates/repo/settings/webhook/list.tmpl | 3 + templates/repo/settings/webhook/msteams.tmpl | 11 + templates/repo/settings/webhook/new.tmpl | 3 + 14 files changed, 825 insertions(+), 1 deletion(-) create mode 100644 models/webhook_msteams.go create mode 100644 public/img/msteams.png create mode 100644 templates/repo/settings/webhook/msteams.tmpl diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 6f732307a..617301986 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -122,4 +122,5 @@ _Symbols used in table:_ | Two factor authentication (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | | Mattermost/Slack integration | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ | | Discord integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Microsoft Teams integration | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | External CI/CD status display | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/models/webhook.go b/models/webhook.go index 8db281a15..9be89241a 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -466,6 +466,7 @@ const ( DISCORD DINGTALK TELEGRAM + MSTEAMS ) var hookTaskTypes = map[string]HookTaskType{ @@ -475,6 +476,7 @@ var hookTaskTypes = map[string]HookTaskType{ "discord": DISCORD, "dingtalk": DINGTALK, "telegram": TELEGRAM, + "msteams": MSTEAMS, } // ToHookTaskType returns HookTaskType by given name. @@ -497,6 +499,8 @@ func (t HookTaskType) Name() string { return "dingtalk" case TELEGRAM: return "telegram" + case MSTEAMS: + return "msteams" } return "" } @@ -675,6 +679,11 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, if err != nil { return fmt.Errorf("GetTelegramPayload: %v", err) } + case MSTEAMS: + payloader, err = GetMSTeamsPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetMSTeamsPayload: %v", err) + } default: p.SetSecret(w.Secret) payloader = p diff --git a/models/webhook_msteams.go b/models/webhook_msteams.go new file mode 100644 index 000000000..b0fc4e7a2 --- /dev/null +++ b/models/webhook_msteams.go @@ -0,0 +1,703 @@ +// 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/sdk/gitea" +) + +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", successColor), + 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", warnColor), + 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", successColor), + 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", successColor), + 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: "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 = warnColor + case api.HookIssueClosed: + title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + color = failedColor + text = p.Issue.Body + case api.HookIssueReOpened: + title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueEdited: + title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueAssigned: + title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName, + p.Issue.Assignee.UserName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = successColor + case api.HookIssueUnassigned: + title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueLabelUpdated: + title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueLabelCleared: + title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueSynchronized: + title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + case api.HookIssueMilestoned: + title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) + text = p.Issue.Body + color = warnColor + 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 = warnColor + } + + 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: + title = "New comment: " + title + content = p.Comment.Body + color = successColor + case api.HookIssueCommentEdited: + title = "Comment edited: " + title + content = p.Comment.Body + color = warnColor + case api.HookIssueCommentDeleted: + title = "Comment deleted: " + title + url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) + content = p.Comment.Body + color = warnColor + } + + return &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 = warnColor + 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 = successColor + } else { + title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) + color = failedColor + } + 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 = warnColor + 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 = warnColor + 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 = successColor + 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 = warnColor + 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 = warnColor + 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 = warnColor + 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 = warnColor + 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 = warnColor + 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 = warnColor + } + + 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.PullRequest.Body + color = warnColor + } + + 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 = successColor + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + color = warnColor + } + + 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 = successColor + case api.HookReleaseUpdated: + title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) + url = p.Release.URL + color = successColor + case api.HookReleaseDeleted: + title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) + url = p.Release.URL + color = successColor + } + + 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/modules/auth/repo_form.go b/modules/auth/repo_form.go index d37a5b94d..fd6891288 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -275,6 +275,17 @@ func (f *NewTelegramHookForm) Validate(ctx *macaron.Context, errs binding.Errors return validate(errs, ctx.Data, f, ctx.Locale) } +// NewMSTeamsHookForm form for creating MS Teams hook +type NewMSTeamsHookForm struct { + PayloadURL string `binding:"Required;ValidUrl"` + WebhookForm +} + +// Validate validates the fields +func (f *NewMSTeamsHookForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { + return validate(errs, ctx.Data, f, ctx.Locale) +} + // .___ // | | ______ ________ __ ____ // | |/ ___// ___/ | \_/ __ \ diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index 0d91e7d9e..b0e7d66ad 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -25,6 +25,6 @@ func newWebhookService() { Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() - Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram"} + Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams"} Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5c391cf56..a0a114ad4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1212,6 +1212,7 @@ settings.slack_channel = Channel settings.add_discord_hook_desc = Integrate Discord into your repository. settings.add_dingtalk_hook_desc = Integrate Dingtalk into your repository. settings.add_telegram_hook_desc = Integrate Telegram into your repository. +settings.add_msteams_hook_desc = Integrate Microsoft Teams into your repository. settings.deploy_keys = Deploy Keys settings.add_deploy_key = Add Deploy Key settings.deploy_key_desc = Deploy keys have read-only pull access to the repository. diff --git a/public/img/msteams.png b/public/img/msteams.png new file mode 100644 index 0000000000000000000000000000000000000000..27313918e1ce7824a1971156d03b2f251da91634 GIT binary patch literal 6154 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 seP~N~o(qn2bHc6&|Nr^9deAk&oE^P {{else if eq .HookType "dingtalk"}} + {{else if eq .HookType "msteams"}} + {{end}} @@ -29,6 +31,7 @@ {{template "repo/settings/webhook/slack" .}} {{template "repo/settings/webhook/discord" .}} {{template "repo/settings/webhook/dingtalk" .}} + {{template "repo/settings/webhook/msteams" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 5b216c466..5db91011d 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -21,6 +21,8 @@ {{else if eq .HookType "telegram"}} + {{else if eq .HookType "msteams"}} + {{end}} @@ -31,6 +33,7 @@ {{template "repo/settings/webhook/discord" .}} {{template "repo/settings/webhook/dingtalk" .}} {{template "repo/settings/webhook/telegram" .}} + {{template "repo/settings/webhook/msteams" .}} {{template "repo/settings/webhook/history" .}} diff --git a/templates/repo/settings/webhook/list.tmpl b/templates/repo/settings/webhook/list.tmpl index ddba0f863..8fdae45b1 100644 --- a/templates/repo/settings/webhook/list.tmpl +++ b/templates/repo/settings/webhook/list.tmpl @@ -23,6 +23,9 @@ Telegram + + Microsoft Teams + diff --git a/templates/repo/settings/webhook/msteams.tmpl b/templates/repo/settings/webhook/msteams.tmpl new file mode 100644 index 000000000..146cf533e --- /dev/null +++ b/templates/repo/settings/webhook/msteams.tmpl @@ -0,0 +1,11 @@ +{{if eq .HookType "msteams"}} +

{{.i18n.Tr "repo.settings.add_msteams_hook_desc" "https://teams.microsoft.com" | Str2html}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+ {{template "repo/settings/webhook/settings" .}} +
+{{end}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index 8c8b3280e..358827c0f 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -19,6 +19,8 @@ {{else if eq .HookType "telegram"}} + {{else if eq .HookType "msteams"}} + {{end}} @@ -29,6 +31,7 @@ {{template "repo/settings/webhook/discord" .}} {{template "repo/settings/webhook/dingtalk" .}} {{template "repo/settings/webhook/telegram" .}} + {{template "repo/settings/webhook/msteams" .}} {{template "repo/settings/webhook/history" .}}