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 6f732307a2..617301986c 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 8db281a15c..9be89241a4 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 0000000000..b0fc4e7a2e --- /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 d37a5b94d8..fd6891288d 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 0d91e7d9e7..b0e7d66ad2 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 5c391cf566..a0a114ad42 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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 {{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 5b216c466b..5db91011d8 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 ddba0f863c..8fdae45b1b 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 0000000000..146cf533e8 --- /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 8c8b3280e5..358827c0f8 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" .}}