mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-02-03 20:51:07 -05:00
Committer names in Slack/Mattermost messages trigger notifications to the corresponding users if they have configured notifications for messages containing their names. These notifications are intended to alert users when someone else mentions them. However, for Git-related messages, users may receive notifications triggered by their own actions. To prevent this, BitBucket, for example, places names in inline code blocks. This pull request adds a similar feature for the Forgejo Slack webhook. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10147 Reviewed-by: Lucas <sclu1034@noreply.codeberg.org> Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Erki Aring <erki@example.ee> Co-committed-by: Erki Aring <erki@example.ee>
374 lines
14 KiB
Go
374 lines
14 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"net/url"
|
|
"strings"
|
|
|
|
webhook_model "forgejo.org/models/webhook"
|
|
"forgejo.org/modules/setting"
|
|
api "forgejo.org/modules/structs"
|
|
"forgejo.org/modules/util"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
)
|
|
|
|
type (
|
|
linkFormatter = func(string, string) string
|
|
nameFormatter = func(string) string
|
|
)
|
|
|
|
// noneLinkFormatter does not create a link but just returns the text
|
|
func noneLinkFormatter(url, text string) string {
|
|
return text
|
|
}
|
|
|
|
// htmlLinkFormatter creates a HTML link
|
|
func htmlLinkFormatter(url, text string) string {
|
|
return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
|
|
}
|
|
|
|
// noneNameFormatter just returns the name
|
|
func noneNameFormatter(name string) string {
|
|
return name
|
|
}
|
|
|
|
// getPullRequestInfo gets the information for a pull request
|
|
func getPullRequestInfo(p *api.PullRequestPayload, nameFormatter nameFormatter) (title, link, by, operator, operateResult, assignees string) {
|
|
title = fmt.Sprintf("[PullRequest-%s #%d]: %s\n%s", p.Repository.FullName, p.PullRequest.Index, p.Action, p.PullRequest.Title)
|
|
assignList := p.PullRequest.Assignees
|
|
assignStringList := make([]string, len(assignList))
|
|
|
|
for i, user := range assignList {
|
|
assignStringList[i] = nameFormatter(user.UserName)
|
|
}
|
|
switch p.Action {
|
|
case api.HookIssueAssigned:
|
|
operateResult = fmt.Sprintf("%s assign this to %s", nameFormatter(p.Sender.UserName), nameFormatter(assignList[len(assignList)-1].UserName))
|
|
case api.HookIssueUnassigned:
|
|
operateResult = fmt.Sprintf("%s unassigned this for someone", nameFormatter(p.Sender.UserName))
|
|
case api.HookIssueMilestoned:
|
|
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
|
|
}
|
|
link = p.PullRequest.HTMLURL
|
|
by = fmt.Sprintf("PullRequest by %s", nameFormatter(p.PullRequest.Poster.UserName))
|
|
if len(assignStringList) > 0 {
|
|
assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
|
|
}
|
|
operator = fmt.Sprintf("Operator: %s", nameFormatter(p.Sender.UserName))
|
|
return title, link, by, operator, operateResult, assignees
|
|
}
|
|
|
|
// getIssuesInfo gets the information for an issue
|
|
func getIssuesInfo(p *api.IssuePayload, nameFormatter nameFormatter) (issueTitle, link, by, operator, operateResult, assignees string) {
|
|
issueTitle = fmt.Sprintf("[Issue-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
|
assignList := p.Issue.Assignees
|
|
assignStringList := make([]string, len(assignList))
|
|
|
|
for i, user := range assignList {
|
|
assignStringList[i] = nameFormatter(user.UserName)
|
|
}
|
|
switch p.Action {
|
|
case api.HookIssueAssigned:
|
|
operateResult = fmt.Sprintf("%s assign this to %s", nameFormatter(p.Sender.UserName), nameFormatter(assignList[len(assignList)-1].UserName))
|
|
case api.HookIssueUnassigned:
|
|
operateResult = fmt.Sprintf("%s unassigned this for someone", nameFormatter(p.Sender.UserName))
|
|
case api.HookIssueMilestoned:
|
|
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
|
|
}
|
|
link = p.Issue.HTMLURL
|
|
by = fmt.Sprintf("Issue by %s", nameFormatter(p.Issue.Poster.UserName))
|
|
if len(assignStringList) > 0 {
|
|
assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
|
|
}
|
|
operator = fmt.Sprintf("Operator: %s", nameFormatter(p.Sender.UserName))
|
|
return issueTitle, link, by, operator, operateResult, assignees
|
|
}
|
|
|
|
// getIssuesCommentInfo gets the information for a comment
|
|
func getIssuesCommentInfo(p *api.IssueCommentPayload, nameFormatter nameFormatter) (title, link, by, operator string) {
|
|
title = fmt.Sprintf("[Comment-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
|
link = p.Issue.HTMLURL
|
|
if p.IsPull {
|
|
by = fmt.Sprintf("PullRequest by %s", nameFormatter(p.Issue.Poster.UserName))
|
|
} else {
|
|
by = fmt.Sprintf("Issue by %s", nameFormatter(p.Issue.Poster.UserName))
|
|
}
|
|
operator = fmt.Sprintf("Operator: %s", nameFormatter(p.Sender.UserName))
|
|
return title, link, by, operator
|
|
}
|
|
|
|
func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (string, string, string, int) {
|
|
issueTitle := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
|
|
titleLink := linkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index), issueTitle)
|
|
var text string
|
|
color := yellowColor
|
|
|
|
switch p.Action {
|
|
case api.HookIssueOpened:
|
|
text = fmt.Sprintf("[%s] Issue opened: %s", p.Repository.FullName, titleLink)
|
|
color = orangeColor
|
|
case api.HookIssueClosed:
|
|
text = fmt.Sprintf("[%s] Issue closed: %s", p.Repository.FullName, titleLink)
|
|
color = redColor
|
|
case api.HookIssueReOpened:
|
|
text = fmt.Sprintf("[%s] Issue re-opened: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueEdited:
|
|
text = fmt.Sprintf("[%s] Issue edited: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueAssigned:
|
|
list := make([]string, len(p.Issue.Assignees))
|
|
for i, user := range p.Issue.Assignees {
|
|
list[i] = linkFormatter(setting.AppURL+url.PathEscape(user.UserName), user.UserName)
|
|
}
|
|
text = fmt.Sprintf("[%s] Issue assigned to %s: %s", p.Repository.FullName, strings.Join(list, ", "), titleLink)
|
|
color = greenColor
|
|
case api.HookIssueUnassigned:
|
|
text = fmt.Sprintf("[%s] Issue unassigned: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueLabelUpdated:
|
|
text = fmt.Sprintf("[%s] Issue labels updated: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueLabelCleared:
|
|
text = fmt.Sprintf("[%s] Issue labels cleared: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueSynchronized:
|
|
text = fmt.Sprintf("[%s] Issue synchronized: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueMilestoned:
|
|
text = fmt.Sprintf("[%s] Issue milestoned to %s: %s", p.Repository.FullName, p.Issue.Milestone.Title, titleLink)
|
|
case api.HookIssueDemilestoned:
|
|
text = fmt.Sprintf("[%s] Issue milestone cleared: %s", p.Repository.FullName, titleLink)
|
|
}
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
var attachmentText string
|
|
if p.Action == api.HookIssueOpened || p.Action == api.HookIssueEdited {
|
|
attachmentText = p.Issue.Body
|
|
}
|
|
|
|
return text, issueTitle, attachmentText, color
|
|
}
|
|
|
|
func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (string, string, string, int) {
|
|
issueTitle := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
|
|
titleLink := linkFormatter(p.PullRequest.URL, issueTitle)
|
|
var text string
|
|
var attachmentText string
|
|
color := yellowColor
|
|
|
|
switch p.Action {
|
|
case api.HookIssueOpened:
|
|
text = fmt.Sprintf("[%s] Pull request opened: %s", p.Repository.FullName, titleLink)
|
|
attachmentText = p.PullRequest.Body
|
|
color = greenColor
|
|
case api.HookIssueClosed:
|
|
if p.PullRequest.HasMerged {
|
|
text = fmt.Sprintf("[%s] Pull request merged: %s", p.Repository.FullName, titleLink)
|
|
color = purpleColor
|
|
} else {
|
|
text = fmt.Sprintf("[%s] Pull request closed: %s", p.Repository.FullName, titleLink)
|
|
color = redColor
|
|
}
|
|
case api.HookIssueReOpened:
|
|
text = fmt.Sprintf("[%s] Pull request re-opened: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueEdited:
|
|
text = fmt.Sprintf("[%s] Pull request edited: %s", p.Repository.FullName, titleLink)
|
|
attachmentText = p.PullRequest.Body
|
|
case api.HookIssueAssigned:
|
|
list := make([]string, len(p.PullRequest.Assignees))
|
|
for i, user := range p.PullRequest.Assignees {
|
|
list[i] = linkFormatter(setting.AppURL+user.UserName, user.UserName)
|
|
}
|
|
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s", p.Repository.FullName,
|
|
strings.Join(list, ", "), titleLink)
|
|
color = greenColor
|
|
case api.HookIssueUnassigned:
|
|
text = fmt.Sprintf("[%s] Pull request unassigned: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueLabelUpdated:
|
|
text = fmt.Sprintf("[%s] Pull request labels updated: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueLabelCleared:
|
|
text = fmt.Sprintf("[%s] Pull request labels cleared: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueSynchronized:
|
|
text = fmt.Sprintf("[%s] Pull request synchronized: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueMilestoned:
|
|
text = fmt.Sprintf("[%s] Pull request milestoned to %s: %s", p.Repository.FullName, p.PullRequest.Milestone.Title, titleLink)
|
|
case api.HookIssueDemilestoned:
|
|
text = fmt.Sprintf("[%s] Pull request milestone cleared: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueReviewed:
|
|
text = fmt.Sprintf("[%s] Pull request reviewed: %s", p.Repository.FullName, titleLink)
|
|
attachmentText = p.Review.Content
|
|
case api.HookIssueReviewRequested:
|
|
text = fmt.Sprintf("[%s] Pull request review requested: %s", p.Repository.FullName, titleLink)
|
|
case api.HookIssueReviewRequestRemoved:
|
|
text = fmt.Sprintf("[%s] Pull request review request removed: %s", p.Repository.FullName, titleLink)
|
|
}
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
return text, issueTitle, attachmentText, color
|
|
}
|
|
|
|
func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (text string, color int) {
|
|
refLink := linkFormatter(p.Repository.HTMLURL+"/releases/tag/"+util.PathEscapeSegments(p.Release.TagName), p.Release.TagName)
|
|
|
|
switch p.Action {
|
|
case api.HookReleasePublished:
|
|
text = fmt.Sprintf("[%s] Release created: %s", p.Repository.FullName, refLink)
|
|
color = greenColor
|
|
case api.HookReleaseUpdated:
|
|
text = fmt.Sprintf("[%s] Release updated: %s", p.Repository.FullName, refLink)
|
|
color = yellowColor
|
|
case api.HookReleaseDeleted:
|
|
text = fmt.Sprintf("[%s] Release deleted: %s", p.Repository.FullName, refLink)
|
|
color = redColor
|
|
}
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
return text, color
|
|
}
|
|
|
|
func getWikiPayloadInfo(p *api.WikiPayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (string, int, string) {
|
|
pageLink := linkFormatter(p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page), p.Page)
|
|
|
|
var text string
|
|
color := greenColor
|
|
|
|
switch p.Action {
|
|
case api.HookWikiCreated:
|
|
text = fmt.Sprintf("[%s] New wiki page '%s'", p.Repository.FullName, pageLink)
|
|
case api.HookWikiEdited:
|
|
text = fmt.Sprintf("[%s] Wiki page '%s' edited", p.Repository.FullName, pageLink)
|
|
color = yellowColor
|
|
case api.HookWikiDeleted:
|
|
text = fmt.Sprintf("[%s] Wiki page '%s' deleted", p.Repository.FullName, pageLink)
|
|
color = redColor
|
|
}
|
|
|
|
if p.Action != api.HookWikiDeleted && p.Comment != "" {
|
|
text += fmt.Sprintf(" (%s)", p.Comment)
|
|
}
|
|
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
return text, color, pageLink
|
|
}
|
|
|
|
func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (string, string, int) {
|
|
issueTitle := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
|
|
|
|
var text, typ, titleLink string
|
|
color := yellowColor
|
|
|
|
if p.IsPull {
|
|
typ = "pull request"
|
|
titleLink = linkFormatter(p.Comment.PRURL, issueTitle)
|
|
} else {
|
|
typ = "issue"
|
|
titleLink = linkFormatter(p.Comment.IssueURL, issueTitle)
|
|
}
|
|
|
|
switch p.Action {
|
|
case api.HookIssueCommentCreated:
|
|
text = fmt.Sprintf("[%s] New comment on %s %s", p.Repository.FullName, typ, titleLink)
|
|
if p.IsPull {
|
|
color = greenColorLight
|
|
} else {
|
|
color = orangeColorLight
|
|
}
|
|
case api.HookIssueCommentEdited:
|
|
text = fmt.Sprintf("[%s] Comment edited on %s %s", p.Repository.FullName, typ, titleLink)
|
|
case api.HookIssueCommentDeleted:
|
|
text = fmt.Sprintf("[%s] Comment deleted on %s %s", p.Repository.FullName, typ, titleLink)
|
|
color = redColor
|
|
}
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
return text, issueTitle, color
|
|
}
|
|
|
|
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, nameFormatter nameFormatter, withSender bool) (text string, color int) {
|
|
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
|
|
|
|
switch p.Action {
|
|
case api.HookPackageCreated:
|
|
text = fmt.Sprintf("Package created: %s", refLink)
|
|
color = greenColor
|
|
case api.HookPackageDeleted:
|
|
text = fmt.Sprintf("Package deleted: %s", refLink)
|
|
color = redColor
|
|
}
|
|
if withSender {
|
|
text += fmt.Sprintf(" by %s", nameFormatter(p.Sender.UserName))
|
|
}
|
|
|
|
return text, color
|
|
}
|
|
|
|
func getActionPayloadInfo(p *api.ActionPayload, linkFormatter linkFormatter) (text string, color int) {
|
|
runLink := linkFormatter(p.Run.HTMLURL, p.Run.Title)
|
|
repoLink := linkFormatter(p.Run.Repo.HTMLURL, p.Run.Repo.FullName)
|
|
|
|
switch p.Action {
|
|
case api.HookActionFailure:
|
|
text = fmt.Sprintf("%s Action Failed in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
|
color = redColor
|
|
case api.HookActionRecover:
|
|
text = fmt.Sprintf("%s Action Recovered in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
|
color = greenColor
|
|
case api.HookActionSuccess:
|
|
text = fmt.Sprintf("%s Action Succeeded in %s %s", runLink, repoLink, p.Run.PrettyRef)
|
|
color = greenColor
|
|
}
|
|
|
|
return text, color
|
|
}
|
|
|
|
// ToHook convert models.Webhook to api.Hook
|
|
// This function is not part of the convert package to prevent an import cycle
|
|
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
|
// config is deprecated, but kept for compatibility
|
|
config := map[string]string{
|
|
"url": w.URL,
|
|
"content_type": w.ContentType.Name(),
|
|
}
|
|
if w.Type == webhook_module.SLACK {
|
|
if s, ok := (slackHandler{}.Metadata(w)).(*SlackMeta); ok {
|
|
config["channel"] = s.Channel
|
|
config["username"] = s.Username
|
|
config["icon_url"] = s.IconURL
|
|
config["color"] = s.Color
|
|
}
|
|
}
|
|
|
|
authorizationHeader, err := w.HeaderAuthorization()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var metadata any
|
|
if handler := GetWebhookHandler(w.Type); handler != nil {
|
|
metadata = handler.Metadata(w)
|
|
}
|
|
|
|
return &api.Hook{
|
|
ID: w.ID,
|
|
Type: w.Type,
|
|
BranchFilter: w.BranchFilter,
|
|
URL: w.URL,
|
|
Config: config,
|
|
Events: w.EventsArray(),
|
|
AuthorizationHeader: authorizationHeader,
|
|
ContentType: w.ContentType.Name(),
|
|
Metadata: metadata,
|
|
Active: w.IsActive,
|
|
Updated: w.UpdatedUnix.AsTime(),
|
|
Created: w.CreatedUnix.AsTime(),
|
|
}, nil
|
|
}
|