2014-08-24 08:59:47 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-08-24 08:59:47 -04:00
2019-11-04 06:13:25 +08:00
package webhook
2014-08-24 08:59:47 -04:00
import (
2024-03-07 23:18:38 +01:00
"context"
2014-08-24 08:59:47 -04:00
"fmt"
2024-03-07 23:18:38 +01:00
"net/http"
2022-08-11 17:48:23 +02:00
"regexp"
2014-08-24 08:59:47 -04:00
"strings"
2015-08-28 23:36:13 +08:00
2021-11-10 13:13:16 +08:00
webhook_model "code.gitea.io/gitea/models/webhook"
2019-03-27 17:33:00 +08:00
"code.gitea.io/gitea/modules/git"
2021-07-25 00:03:58 +08:00
"code.gitea.io/gitea/modules/json"
2019-11-04 06:13:25 +08:00
"code.gitea.io/gitea/modules/log"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2019-05-11 18:21:34 +08:00
api "code.gitea.io/gitea/modules/structs"
2023-01-01 16:23:15 +01:00
webhook_module "code.gitea.io/gitea/modules/webhook"
2014-08-24 08:59:47 -04:00
)
2017-01-04 19:50:34 -05:00
// SlackMeta contains the slack metadata
2015-08-28 23:36:13 +08:00
type SlackMeta struct {
2015-08-29 11:49:59 +08:00
Channel string ` json:"channel" `
Username string ` json:"username" `
IconURL string ` json:"icon_url" `
Color string ` json:"color" `
2014-08-24 08:59:47 -04:00
}
2019-11-04 06:13:25 +08:00
// GetSlackHook returns slack metadata
2021-11-10 13:13:16 +08:00
func GetSlackHook ( w * webhook_model . Webhook ) * SlackMeta {
2019-11-04 06:13:25 +08:00
s := & SlackMeta { }
if err := json . Unmarshal ( [ ] byte ( w . Meta ) , s ) ; err != nil {
log . Error ( "webhook.GetSlackHook(%d): %v" , w . ID , err )
}
return s
}
2016-11-24 22:35:47 +01:00
// SlackPayload contains the information about the slack channel
2014-08-24 08:59:47 -04:00
type SlackPayload struct {
Channel string ` json:"channel" `
Text string ` json:"text" `
Username string ` json:"username" `
2015-08-29 11:49:59 +08:00
IconURL string ` json:"icon_url" `
2014-08-24 08:59:47 -04:00
UnfurlLinks int ` json:"unfurl_links" `
LinkNames int ` json:"link_names" `
Attachments [ ] SlackAttachment ` json:"attachments" `
}
2016-11-24 22:35:47 +01:00
// SlackAttachment contains the slack message
2014-08-24 08:59:47 -04:00
type SlackAttachment struct {
2019-12-18 18:01:00 +08:00
Fallback string ` json:"fallback" `
Color string ` json:"color" `
Title string ` json:"title" `
TitleLink string ` json:"title_link" `
Text string ` json:"text" `
2014-08-24 08:59:47 -04:00
}
2016-11-24 22:35:47 +01:00
// SlackTextFormatter replaces &, <, > with HTML characters
2015-08-28 23:36:13 +08:00
// see: https://api.slack.com/docs/formatting
func SlackTextFormatter ( s string ) string {
// replace & < >
2020-10-11 22:27:20 +02:00
s = strings . ReplaceAll ( s , "&" , "&" )
s = strings . ReplaceAll ( s , "<" , "<" )
s = strings . ReplaceAll ( s , ">" , ">" )
2016-08-14 03:32:24 -07:00
return s
}
2016-11-24 22:35:47 +01:00
// SlackShortTextFormatter replaces &, <, > with HTML characters
2016-08-14 03:32:24 -07:00
func SlackShortTextFormatter ( s string ) string {
s = strings . Split ( s , "\n" ) [ 0 ]
// replace & < >
2020-10-11 22:27:20 +02:00
s = strings . ReplaceAll ( s , "&" , "&" )
s = strings . ReplaceAll ( s , "<" , "<" )
s = strings . ReplaceAll ( s , ">" , ">" )
2016-08-14 03:32:24 -07:00
return s
2015-08-28 23:36:13 +08:00
}
2014-08-24 08:59:47 -04:00
2017-01-04 19:50:34 -05:00
// SlackLinkFormatter creates a link compatible with slack
2021-12-20 05:41:31 +01:00
func SlackLinkFormatter ( url , text string ) string {
2015-08-28 23:36:13 +08:00
return fmt . Sprintf ( "<%s|%s>" , url , SlackTextFormatter ( text ) )
2014-08-24 08:59:47 -04:00
}
2017-10-29 19:04:25 -07:00
// SlackLinkToRef slack-formatter link to a repo ref
func SlackLinkToRef ( repoURL , ref string ) string {
2023-12-13 21:02:00 +00:00
// FIXME: SHA1 hardcoded here
2020-05-15 00:55:43 +02:00
url := git . RefURL ( repoURL , ref )
2023-05-26 09:04:48 +08:00
refName := git . RefName ( ref ) . ShortName ( )
2020-05-15 00:55:43 +02:00
return SlackLinkFormatter ( url , refName )
2017-10-29 19:04:25 -07:00
}
2015-08-28 23:36:13 +08:00
2024-03-07 23:18:38 +01:00
// Create implements payloadConvertor Create method
func ( s slackConvertor ) Create ( p * api . CreatePayload ) ( SlackPayload , error ) {
2019-11-30 12:23:37 +02:00
repoLink := SlackLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
2017-10-29 19:04:25 -07:00
refLink := SlackLinkToRef ( p . Repo . HTMLURL , p . Ref )
2015-08-28 23:36:13 +08:00
text := fmt . Sprintf ( "[%s:%s] %s created by %s" , repoLink , refLink , p . RefType , p . Sender . UserName )
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
2015-08-28 23:36:13 +08:00
}
2020-09-05 10:57:13 +08:00
// Delete composes Slack payload for delete a branch or tag.
2024-03-07 23:18:38 +01:00
func ( s slackConvertor ) Delete ( p * api . DeletePayload ) ( SlackPayload , error ) {
2023-05-26 09:04:48 +08:00
refName := git . RefName ( p . Ref ) . ShortName ( )
2019-11-30 12:23:37 +02:00
repoLink := SlackLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
2018-05-16 22:01:55 +08:00
text := fmt . Sprintf ( "[%s:%s] %s deleted by %s" , repoLink , refName , p . RefType , p . Sender . UserName )
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
2018-05-16 22:01:55 +08:00
}
2020-09-05 10:57:13 +08:00
// Fork composes Slack payload for forked by a repository.
2024-03-07 23:18:38 +01:00
func ( s slackConvertor ) Fork ( p * api . ForkPayload ) ( SlackPayload , error ) {
2019-08-07 02:27:10 +03:00
baseLink := SlackLinkFormatter ( p . Forkee . HTMLURL , p . Forkee . FullName )
forkLink := SlackLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
2018-05-16 22:01:55 +08:00
text := fmt . Sprintf ( "%s is forked to %s" , baseLink , forkLink )
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
2018-05-16 22:01:55 +08:00
}
2024-03-07 23:18:38 +01:00
// Issue implements payloadConvertor Issue method
func ( s slackConvertor ) Issue ( p * api . IssuePayload ) ( SlackPayload , error ) {
2024-07-10 19:37:16 +08:00
text , issueTitle , extraMarkdown , color := getIssuesPayloadInfo ( p , SlackLinkFormatter , true )
2018-05-16 22:01:55 +08:00
2021-06-21 04:12:19 +02:00
var attachments [ ] SlackAttachment
2024-07-10 19:37:16 +08:00
if extraMarkdown != "" {
extraMarkdown = SlackTextFormatter ( extraMarkdown )
2019-12-28 16:55:09 +08:00
issueTitle = SlackTextFormatter ( issueTitle )
2021-06-21 04:12:19 +02:00
attachments = append ( attachments , SlackAttachment {
2019-12-28 16:55:09 +08:00
Color : fmt . Sprintf ( "%x" , color ) ,
Title : issueTitle ,
2020-01-08 17:10:34 -06:00
TitleLink : p . Issue . HTMLURL ,
2024-07-10 19:37:16 +08:00
Text : extraMarkdown ,
2021-06-21 04:12:19 +02:00
} )
2019-12-18 18:01:00 +08:00
}
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , attachments ) , nil
2018-05-16 22:01:55 +08:00
}
2024-03-07 23:18:38 +01:00
// IssueComment implements payloadConvertor IssueComment method
func ( s slackConvertor ) IssueComment ( p * api . IssueCommentPayload ) ( SlackPayload , error ) {
2020-01-04 16:20:15 -06:00
text , issueTitle , color := getIssueCommentPayloadInfo ( p , SlackLinkFormatter , true )
2018-05-16 22:01:55 +08:00
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , [ ] SlackAttachment { {
Color : fmt . Sprintf ( "%x" , color ) ,
Title : issueTitle ,
TitleLink : p . Comment . HTMLURL ,
Text : SlackTextFormatter ( p . Comment . Body ) ,
} } ) , nil
2018-05-16 22:01:55 +08:00
}
2024-03-07 23:18:38 +01:00
// Wiki implements payloadConvertor Wiki method
func ( s slackConvertor ) Wiki ( p * api . WikiPayload ) ( SlackPayload , error ) {
2022-09-04 21:54:23 +02:00
text , _ , _ := getWikiPayloadInfo ( p , SlackLinkFormatter , true )
return s . createPayload ( text , nil ) , nil
}
2024-03-07 23:18:38 +01:00
// Release implements payloadConvertor Release method
func ( s slackConvertor ) Release ( p * api . ReleasePayload ) ( SlackPayload , error ) {
2020-01-04 16:20:15 -06:00
text , _ := getReleasePayloadInfo ( p , SlackLinkFormatter , true )
2018-05-21 10:28:29 +08:00
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
2018-05-16 22:01:55 +08:00
}
2024-03-07 23:18:38 +01:00
func ( s slackConvertor ) Package ( p * api . PackagePayload ) ( SlackPayload , error ) {
2023-10-31 12:43:38 +08:00
text , _ := getPackagePayloadInfo ( p , SlackLinkFormatter , true )
return s . createPayload ( text , nil ) , nil
}
2024-03-07 23:18:38 +01:00
// Push implements payloadConvertor Push method
func ( s slackConvertor ) Push ( p * api . PushPayload ) ( SlackPayload , error ) {
2014-08-24 08:59:47 -04:00
// n new commits
2015-08-28 23:36:13 +08:00
var (
2015-12-04 15:41:56 -05:00
commitDesc string
2015-08-28 23:36:13 +08:00
commitString string
)
2014-08-24 08:59:47 -04:00
2022-10-16 18:22:34 +02:00
if p . TotalCommits == 1 {
2015-12-04 15:41:56 -05:00
commitDesc = "1 new commit"
2014-08-24 08:59:47 -04:00
} else {
2022-10-16 18:22:34 +02:00
commitDesc = fmt . Sprintf ( "%d new commits" , p . TotalCommits )
2015-12-04 15:41:56 -05:00
}
2016-08-14 04:17:26 -07:00
if len ( p . CompareURL ) > 0 {
commitString = SlackLinkFormatter ( p . CompareURL , commitDesc )
2015-12-04 15:41:56 -05:00
} else {
commitString = commitDesc
2014-08-24 08:59:47 -04:00
}
2019-11-30 12:23:37 +02:00
repoLink := SlackLinkFormatter ( p . Repo . HTMLURL , p . Repo . FullName )
2017-10-29 19:04:25 -07:00
branchLink := SlackLinkToRef ( p . Repo . HTMLURL , p . Ref )
2016-08-14 04:17:26 -07:00
text := fmt . Sprintf ( "[%s:%s] %s pushed by %s" , repoLink , branchLink , commitString , p . Pusher . UserName )
2014-08-24 08:59:47 -04:00
2015-08-28 23:36:13 +08:00
var attachmentText string
2014-08-24 08:59:47 -04:00
// for each commit, generate attachment text
for i , commit := range p . Commits {
2016-08-14 03:32:24 -07:00
attachmentText += fmt . Sprintf ( "%s: %s - %s" , SlackLinkFormatter ( commit . URL , commit . ID [ : 7 ] ) , SlackShortTextFormatter ( commit . Message ) , SlackTextFormatter ( commit . Author . Name ) )
2014-08-24 08:59:47 -04:00
// add linebreak to each commit but the last
if i < len ( p . Commits ) - 1 {
attachmentText += "\n"
}
}
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , [ ] SlackAttachment { {
Color : s . Color ,
Title : p . Repo . HTMLURL ,
TitleLink : p . Repo . HTMLURL ,
Text : attachmentText ,
} } ) , nil
2016-08-14 03:32:24 -07:00
}
2024-03-07 23:18:38 +01:00
// PullRequest implements payloadConvertor PullRequest method
func ( s slackConvertor ) PullRequest ( p * api . PullRequestPayload ) ( SlackPayload , error ) {
2024-07-10 19:37:16 +08:00
text , issueTitle , extraMarkdown , color := getPullRequestPayloadInfo ( p , SlackLinkFormatter , true )
2014-08-24 08:59:47 -04:00
2021-06-21 04:12:19 +02:00
var attachments [ ] SlackAttachment
2024-07-10 19:37:16 +08:00
if extraMarkdown != "" {
extraMarkdown = SlackTextFormatter ( p . PullRequest . Body )
2019-12-28 16:55:09 +08:00
issueTitle = SlackTextFormatter ( issueTitle )
2021-06-21 04:12:19 +02:00
attachments = append ( attachments , SlackAttachment {
2019-12-28 16:55:09 +08:00
Color : fmt . Sprintf ( "%x" , color ) ,
Title : issueTitle ,
2023-09-21 17:55:09 -05:00
TitleLink : p . PullRequest . HTMLURL ,
2024-07-10 19:37:16 +08:00
Text : extraMarkdown ,
2021-06-21 04:12:19 +02:00
} )
2019-12-18 18:01:00 +08:00
}
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , attachments ) , nil
2014-08-24 08:59:47 -04:00
}
2024-03-07 23:18:38 +01:00
// Review implements payloadConvertor Review method
func ( s slackConvertor ) Review ( p * api . PullRequestPayload , event webhook_module . HookEventType ) ( SlackPayload , error ) {
2018-12-27 19:04:30 +01:00
senderLink := SlackLinkFormatter ( setting . AppURL + p . Sender . UserName , p . Sender . UserName )
2019-12-18 18:01:00 +08:00
title := fmt . Sprintf ( "#%d %s" , p . Index , p . PullRequest . Title )
titleLink := fmt . Sprintf ( "%s/pulls/%d" , p . Repository . HTMLURL , p . Index )
2019-11-30 12:23:37 +02:00
repoLink := SlackLinkFormatter ( p . Repository . HTMLURL , p . Repository . FullName )
2019-12-18 18:01:00 +08:00
var text string
2018-12-27 19:04:30 +01:00
switch p . Action {
2020-03-05 23:10:48 -06:00
case api . HookIssueReviewed :
2018-12-27 19:04:30 +01:00
action , err := parseHookPullRequestEventType ( event )
if err != nil {
2024-03-07 23:18:38 +01:00
return SlackPayload { } , err
2018-12-27 19:04:30 +01:00
}
2019-12-18 18:01:00 +08:00
text = fmt . Sprintf ( "[%s] Pull request review %s: [%s](%s) by %s" , repoLink , action , title , titleLink , senderLink )
2018-12-27 19:04:30 +01:00
}
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
2018-12-27 19:04:30 +01:00
}
2024-03-07 23:18:38 +01:00
// Repository implements payloadConvertor Repository method
func ( s slackConvertor ) Repository ( p * api . RepositoryPayload ) ( SlackPayload , error ) {
2017-09-03 01:20:24 -07:00
senderLink := SlackLinkFormatter ( setting . AppURL + p . Sender . UserName , p . Sender . UserName )
2019-11-30 12:23:37 +02:00
repoLink := SlackLinkFormatter ( p . Repository . HTMLURL , p . Repository . FullName )
2020-01-21 14:29:24 -06:00
var text string
2019-12-18 18:01:00 +08:00
2017-09-03 01:20:24 -07:00
switch p . Action {
case api . HookRepoCreated :
2019-11-30 12:23:37 +02:00
text = fmt . Sprintf ( "[%s] Repository created by %s" , repoLink , senderLink )
2017-09-03 01:20:24 -07:00
case api . HookRepoDeleted :
2019-11-30 12:23:37 +02:00
text = fmt . Sprintf ( "[%s] Repository deleted by %s" , repoLink , senderLink )
2017-09-03 01:20:24 -07:00
}
2021-06-21 04:12:19 +02:00
return s . createPayload ( text , nil ) , nil
}
2024-03-07 23:18:38 +01:00
func ( s slackConvertor ) createPayload ( text string , attachments [ ] SlackAttachment ) SlackPayload {
return SlackPayload {
2021-06-21 04:12:19 +02:00
Channel : s . Channel ,
Text : text ,
Username : s . Username ,
IconURL : s . IconURL ,
Attachments : attachments ,
}
2017-09-03 01:20:24 -07:00
}
2024-03-07 23:18:38 +01:00
type slackConvertor struct {
Channel string
Username string
IconURL string
Color string
}
2015-08-28 23:36:13 +08:00
2024-06-11 20:47:45 +02:00
func newSlackRequest ( _ context . Context , w * webhook_model . Webhook , t * webhook_model . HookTask ) ( * http . Request , [ ] byte , error ) {
2024-03-07 23:18:38 +01:00
meta := & SlackMeta { }
if err := json . Unmarshal ( [ ] byte ( w . Meta ) , meta ) ; err != nil {
return nil , nil , fmt . Errorf ( "newSlackRequest meta json: %w" , err )
}
2024-07-10 19:37:16 +08:00
var pc payloadConvertor [ SlackPayload ] = slackConvertor {
2024-03-07 23:18:38 +01:00
Channel : meta . Channel ,
Username : meta . Username ,
IconURL : meta . IconURL ,
Color : meta . Color ,
}
2024-07-10 19:37:16 +08:00
return newJSONRequest ( pc , w , t , true )
2014-08-26 08:20:18 -04:00
}
2022-08-11 17:48:23 +02:00
var slackChannel = regexp . MustCompile ( ` ^#?[a-z0-9_-] { 1,80}$ ` )
// IsValidSlackChannel validates a channel name conforms to what slack expects:
// https://api.slack.com/methods/conversations.rename#naming
// Conversation names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less.
// Gitea accepts if it starts with a #.
func IsValidSlackChannel ( name string ) bool {
return slackChannel . MatchString ( name )
}