101 changes: 61 additions & 40 deletions services/webhook/deliver.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,17 @@ import (
"github.com/gobwas/glob"
)

// Deliver deliver hook task
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
if err != nil {
return err
}

defer func() {
err := recover()
if err == nil {
return
}
// There was a panic whilst delivering a hook...
log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
}()

t.IsDelivered = true

var req *http.Request

func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
switch w.HTTPMethod {
case "":
log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
fallthrough
case http.MethodPost:
switch w.ContentType {
case webhook_model.ContentTypeJSON:
req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
if err != nil {
return err
return nil, nil, err
}

req.Header.Set("Content-Type", "application/json")
Expand All @@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {

req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
if err != nil {
return err
return nil, nil, err
}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
default:
return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType)
}
case http.MethodGet:
u, err := url.Parse(w.URL)
if err != nil {
return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
return nil, nil, fmt.Errorf("invalid URL: %w", err)
}
vals := u.Query()
vals["payload"] = []string{t.PayloadContent}
u.RawQuery = vals.Encode()
req, err = http.NewRequest("GET", u.String(), nil)
if err != nil {
return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
return nil, nil, err
}
case http.MethodPut:
switch w.Type {
case webhook_module.MATRIX:
case webhook_module.MATRIX: // used when t.Version == 1
txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
if err != nil {
return err
return nil, nil, err
}
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
if err != nil {
return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
return nil, nil, err
}
default:
return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
}
default:
return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
}

body = []byte(t.PayloadContent)
return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
}

func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
var signatureSHA1 string
var signatureSHA256 string
if len(w.Secret) > 0 {
sig1 := hmac.New(sha1.New, []byte(w.Secret))
sig256 := hmac.New(sha256.New, []byte(w.Secret))
_, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
if len(secret) > 0 {
sig1 := hmac.New(sha1.New, secret)
sig256 := hmac.New(sha256.New, secret)
_, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
if err != nil {
log.Error("prepareWebhooks.sigWrite: %v", err)
// this error should never happen, since the hashes are writing to []byte and always return a nil error.
return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
}
signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
Expand All @@ -136,27 +125,59 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
return nil
}

// Add Authorization Header
authorization, err := w.HeaderAuthorization()
// Deliver creates the [http.Request] (depending on the webhook type), sends it
// and records the status and response.
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
if err != nil {
log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
return err
}
if authorization != "" {
req.Header["Authorization"] = []string{authorization}

defer func() {
err := recover()
if err == nil {
return
}
// There was a panic whilst delivering a hook...
log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
}()

t.IsDelivered = true

newRequest := webhookRequesters[w.Type]
if t.PayloadVersion == 1 || newRequest == nil {
newRequest = newDefaultRequest
}

req, body, err := newRequest(ctx, w, t)
if err != nil {
return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
}

// Record delivery information.
t.RequestInfo = &webhook_model.HookRequest{
URL: req.URL.String(),
HTTPMethod: req.Method,
Headers: map[string]string{},
Body: string(body),
}
for k, vals := range req.Header {
t.RequestInfo.Headers[k] = strings.Join(vals, ",")
}

// Add Authorization Header
authorization, err := w.HeaderAuthorization()
if err != nil {
return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
}
if authorization != "" {
req.Header.Set("Authorization", authorization)
t.RequestInfo.Headers["Authorization"] = "******"
}

t.ResponseInfo = &webhook_model.HookResponse{
Headers: map[string]string{},
}
Expand Down
202 changes: 197 additions & 5 deletions services/webhook/deliver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package webhook

import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

Expand All @@ -16,7 +18,6 @@ import (
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -107,13 +108,15 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)

hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}}
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadVersion: 2,
}

hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
if !assert.NotNil(t, hookTask) {
return
}
assert.NotNil(t, hookTask)

assert.NoError(t, Deliver(context.Background(), hookTask))
select {
Expand All @@ -123,4 +126,193 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
}

assert.True(t, hookTask.IsSucceed)
assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"])
}

func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

done := make(chan struct{}, 1)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
switch r.URL.Path {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
// Version 1
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
assert.Equal(t, "", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, `{"data": 42}`, string(body))

case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
// Version 2
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Len(t, body, 2147)

default:
w.WriteHeader(404)
t.Fatalf("unexpected url path %s", r.URL.Path)
return
}
w.WriteHeader(200)
done <- struct{}{}
}))
t.Cleanup(s.Close)

hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.MATRIX,
URL: s.URL + "/webhook",
HTTPMethod: "PUT",
ContentType: webhook_model.ContentTypeJSON,
Meta: `{"message_type":0}`, // text
}
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))

t.Run("Version 1", func(t *testing.T) {
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: `{"data": 42}`,
PayloadVersion: 1,
}

hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)

assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}

assert.True(t, hookTask.IsSucceed)
})

t.Run("Version 2", func(t *testing.T) {
p := pushTestPayload()
data, err := p.JSONPayload()
assert.NoError(t, err)

hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}

hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)

assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}

assert.True(t, hookTask.IsSucceed)
})
}

func TestWebhookDeliverSpecificTypes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

type hookCase struct {
gotBody chan []byte
}

cases := map[string]hookCase{
webhook_module.SLACK: {
gotBody: make(chan []byte, 1),
},
webhook_module.DISCORD: {
gotBody: make(chan []byte, 1),
},
webhook_module.DINGTALK: {
gotBody: make(chan []byte, 1),
},
webhook_module.TELEGRAM: {
gotBody: make(chan []byte, 1),
},
webhook_module.MSTEAMS: {
gotBody: make(chan []byte, 1),
},
webhook_module.FEISHU: {
gotBody: make(chan []byte, 1),
},
webhook_module.MATRIX: {
gotBody: make(chan []byte, 1),
},
webhook_module.WECHATWORK: {
gotBody: make(chan []byte, 1),
},
webhook_module.PACKAGIST: {
gotBody: make(chan []byte, 1),
},
}

s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)

typ := strings.Split(r.URL.Path, "/")[1] // take first segment (after skipping leading slash)
hc := cases[typ]
require.NotNil(t, hc.gotBody, r.URL.Path)
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
w.WriteHeader(200)
hc.gotBody <- body
}))
t.Cleanup(s.Close)

p := pushTestPayload()
data, err := p.JSONPayload()
assert.NoError(t, err)

for typ, hc := range cases {
typ := typ
hc := hc
t.Run(typ, func(t *testing.T) {
t.Parallel()
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: typ,
URL: s.URL + "/" + typ,
HTTPMethod: "POST",
ContentType: 0, // set to 0 so that falling back to default request fails with "invalid content type"
Meta: "{}",
}
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))

hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}

hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
assert.NotNil(t, hookTask)

assert.NoError(t, Deliver(context.Background(), hookTask))
select {
case gotBody := <-hc.gotBody:
assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "request body was not saved")
case <-time.After(5 * time.Second):
t.Fatal("waited to long for request to happen")
}

assert.True(t, hookTask.IsSucceed)
})
}
}
58 changes: 26 additions & 32 deletions services/webhook/dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package webhook

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
Expand All @@ -22,19 +24,8 @@ type (
DingtalkPayload dingtalk.Payload
)

var _ PayloadConvertor = &DingtalkPayload{}

// JSONPayload Marshals the DingtalkPayload to json
func (d *DingtalkPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

// Create implements PayloadConvertor Create method
func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
Expand All @@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}

// Delete implements PayloadConvertor Delete method
func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
Expand All @@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}

// Fork implements PayloadConvertor Fork method
func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)

return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil
}

// Push implements PayloadConvertor Push method
func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
Expand Down Expand Up @@ -100,42 +91,42 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}

// Issue implements PayloadConvertor Issue method
func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)

return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil
}

// Wiki implements PayloadConvertor Wiki method
func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) {
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)

return createDingtalkPayload(text, text, "view wiki", url), nil
}

// IssueComment implements PayloadConvertor IssueComment method
func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) {
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)

return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil
}

// PullRequest implements PayloadConvertor PullRequest method
func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)

return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil
}

// Review implements PayloadConvertor Review method
func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) {
var text, title string
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return DingtalkPayload{}, err
}

title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
Expand All @@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module
}

// Repository implements PayloadConvertor Repository method
func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) {
switch p.Action {
case api.HookRepoCreated:
title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
case api.HookRepoDeleted:
title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
return &DingtalkPayload{
return DingtalkPayload{
MsgType: "text",
Text: struct {
Content string `json:"content"`
Expand All @@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
}, nil
}

return nil, nil
return DingtalkPayload{}, nil
}

// Release implements PayloadConvertor Release method
func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)

return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
}

func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)

return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}

func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
return &DingtalkPayload{
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
return DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: strings.TrimSpace(text),
Expand All @@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk
}
}

// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(DingtalkPayload), p, event)
type dingtalkConvertor struct{}

var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}

func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
return newJSONRequest(dingtalkConvertor{}, w, t, true)
}
243 changes: 112 additions & 131 deletions services/webhook/dingtalk_test.go

Large diffs are not rendered by default.

68 changes: 32 additions & 36 deletions services/webhook/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package webhook

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
Expand Down Expand Up @@ -98,19 +100,8 @@ var (
redColor = color("ff3232")
)

// JSONPayload Marshals the DiscordPayload to json
func (d *DiscordPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

var _ PayloadConvertor = &DiscordPayload{}

// Create implements PayloadConvertor Create method
func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
Expand All @@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}

// Delete implements PayloadConvertor Delete method
func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) {
// deleted tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
Expand All @@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}

// Fork implements PayloadConvertor Fork method
func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)

return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
}

// Push implements PayloadConvertor Push method
func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
Expand Down Expand Up @@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}

// Issue implements PayloadConvertor Issue method
func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)

return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil
}

// IssueComment implements PayloadConvertor IssueComment method
func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) {
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)

return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
}

// PullRequest implements PayloadConvertor PullRequest method
func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)

return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
}

// Review implements PayloadConvertor Review method
func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) {
var text, title string
var color int
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return DiscordPayload{}, err
}

title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
Expand All @@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.
}

// Repository implements PayloadConvertor Repository method
func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
var title, url string
var color int
switch p.Action {
Expand All @@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
}

// Wiki implements PayloadConvertor Wiki method
func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)

Expand All @@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
}

// Release implements PayloadConvertor Release method
func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) {
text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)

return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
}

func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)

return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}

// GetDiscordPayload converts a discord webhook into a DiscordPayload
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(DiscordPayload)
type discordConvertor struct {
Username string
AvatarURL string
}

discord := &DiscordMeta{}
if err := json.Unmarshal([]byte(meta), &discord); err != nil {
return s, errors.New("GetDiscordPayload meta json:" + err.Error())
}
s.Username = discord.Username
s.AvatarURL = discord.IconURL
var _ payloadConvertor[DiscordPayload] = discordConvertor{}

return convertPayloader(s, p, event)
func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &DiscordMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
}
sc := discordConvertor{
Username: meta.Username,
AvatarURL: meta.IconURL,
}
return newJSONRequest(sc, w, t, true)
}

func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
Expand All @@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
}
}

func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload {
return &DiscordPayload{
func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
return DiscordPayload{
Username: d.Username,
AvatarURL: d.AvatarURL,
Embeds: []DiscordEmbed{
Expand Down
366 changes: 174 additions & 192 deletions services/webhook/discord_test.go

Large diffs are not rendered by default.

76 changes: 31 additions & 45 deletions services/webhook/feishu.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
package webhook

import (
"context"
"fmt"
"net/http"
"strings"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)
Expand All @@ -23,8 +25,8 @@ type (
}
)

func newFeishuTextPayload(text string) *FeishuPayload {
return &FeishuPayload{
func newFeishuTextPayload(text string) FeishuPayload {
return FeishuPayload{
MsgType: "text",
Content: struct {
Text string `json:"text"`
Expand All @@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload {
}
}

// JSONPayload Marshals the FeishuPayload to json
func (f *FeishuPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(f, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

var _ PayloadConvertor = &FeishuPayload{}

// Create implements PayloadConvertor Create method
func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
Expand All @@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}

// Delete implements PayloadConvertor Delete method
func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
Expand All @@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}

// Fork implements PayloadConvertor Fork method
func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) {
text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)

return newFeishuTextPayload(text), nil
}

// Push implements PayloadConvertor Push method
func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
Expand All @@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}

// Issue implements PayloadConvertor Issue method
func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
title, link, by, operator, result, assignees := getIssuesInfo(p)
var res api.Payloader
if assignees != "" {
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body))
} else {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body))
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil
}
} else {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body))
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil
}
return res, nil
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil
}

// IssueComment implements PayloadConvertor IssueComment method
func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) {
title, link, by, operator := getIssuesCommentInfo(p)
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
}

// PullRequest implements PayloadConvertor PullRequest method
func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
title, link, by, operator, result, assignees := getPullRequestInfo(p)
var res api.Payloader
if assignees != "" {
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body))
} else {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body))
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil
}
} else {
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body))
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil
}
return res, nil
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil
}

// Review implements PayloadConvertor Review method
func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return FeishuPayload{}, err
}

title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
Expand All @@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H
}

// Repository implements PayloadConvertor Repository method
func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
var text string
switch p.Action {
case api.HookRepoCreated:
Expand All @@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
return newFeishuTextPayload(text), nil
}

return nil, nil
return FeishuPayload{}, nil
}

// Wiki implements PayloadConvertor Wiki method
func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)

return newFeishuTextPayload(text), nil
}

// Release implements PayloadConvertor Release method
func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)

return newFeishuTextPayload(text), nil
}

func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)

return newFeishuTextPayload(text), nil
}

// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(FeishuPayload), p, event)
type feishuConvertor struct{}

var _ payloadConvertor[FeishuPayload] = feishuConvertor{}

func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
return newJSONRequest(feishuConvertor{}, w, t, true)
}
147 changes: 64 additions & 83 deletions services/webhook/feishu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package webhook

import (
"context"
"testing"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"

Expand All @@ -14,199 +17,177 @@ import (
)

func TestFeishuPayload(t *testing.T) {
fc := feishuConvertor{}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()

d := new(FeishuPayload)
pl, err := d.Create(p)
pl, err := fc.Create(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text)
assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text)
})

t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()

d := new(FeishuPayload)
pl, err := d.Delete(p)
pl, err := fc.Delete(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text)
assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text)
})

t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()

d := new(FeishuPayload)
pl, err := d.Fork(p)
pl, err := fc.Fork(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text)
assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text)
})

t.Run("Push", func(t *testing.T) {
p := pushTestPayload()

d := new(FeishuPayload)
pl, err := d.Push(p)
pl, err := fc.Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text)
})

t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()

d := new(FeishuPayload)
p.Action = api.HookIssueOpened
pl, err := d.Issue(p)
pl, err := fc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)

p.Action = api.HookIssueClosed
pl, err = d.Issue(p)
pl, err = fc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
})

t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()

d := new(FeishuPayload)
pl, err := d.IssueComment(p)
pl, err := fc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text)
})

t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()

d := new(FeishuPayload)
pl, err := d.PullRequest(p)
pl, err := fc.PullRequest(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text)
})

t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()

d := new(FeishuPayload)
pl, err := d.IssueComment(p)
pl, err := fc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text)
})

t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed

d := new(FeishuPayload)
pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text)
})

t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()

d := new(FeishuPayload)
pl, err := d.Repository(p)
pl, err := fc.Repository(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] Repository created", pl.Content.Text)
})

t.Run("Package", func(t *testing.T) {
p := packageTestPayload()

d := new(FeishuPayload)
pl, err := d.Package(p)
pl, err := fc.Package(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text)
})

t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()

d := new(FeishuPayload)
p.Action = api.HookWikiCreated
pl, err := d.Wiki(p)
pl, err := fc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text)

p.Action = api.HookWikiEdited
pl, err = d.Wiki(p)
pl, err = fc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text)

p.Action = api.HookWikiDeleted
pl, err = d.Wiki(p)
pl, err = fc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text)
})

t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()

d := new(FeishuPayload)
pl, err := d.Release(p)
pl, err := fc.Release(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text)
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text)
})
}

func TestFeishuJSONPayload(t *testing.T) {
p := pushTestPayload()

pl, err := new(FeishuPayload).Push(p)
data, err := p.JSONPayload()
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)

json, err := pl.JSONPayload()
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.FEISHU,
URL: "https://feishu.example.com/",
Meta: `{}`,
HTTPMethod: "POST",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}

req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
assert.NotEmpty(t, json)

assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body FeishuPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
}
213 changes: 110 additions & 103 deletions services/webhook/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
package webhook

import (
"bytes"
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"html"
"net/http"
"net/url"
"regexp"
"strings"
Expand All @@ -23,6 +24,37 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
)

func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &MatrixMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err)
}
mc := matrixConvertor{
MsgType: messageTypeText[meta.MessageType],
}
payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType)
if err != nil {
return nil, nil, err
}

body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
}

txnID, err := getMatrixTxnID(body)
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")

return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
}

const matrixPayloadSizeLimit = 1024 * 64

// MatrixMeta contains the Matrix metadata
Expand All @@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
return s
}

var _ PayloadConvertor = &MatrixPayload{}

// MatrixPayload contains payload for a Matrix room
type MatrixPayload struct {
Body string `json:"body"`
Expand All @@ -57,90 +87,79 @@ type MatrixPayload struct {
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
}

// JSONPayload Marshals the MatrixPayload to json
func (m *MatrixPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}
var _ payloadConvertor[MatrixPayload] = matrixConvertor{}

// MatrixLinkFormatter creates a link compatible with Matrix
func MatrixLinkFormatter(url, text string) string {
return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
type matrixConvertor struct {
MsgType string
}

// MatrixLinkToRef Matrix-formatter link to a repo ref
func MatrixLinkToRef(repoURL, ref string) string {
refName := git.RefName(ref).ShortName()
switch {
case strings.HasPrefix(ref, git.BranchPrefix):
return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
case strings.HasPrefix(ref, git.TagPrefix):
return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
default:
return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
}
func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) {
return MatrixPayload{
Body: getMessageBody(text),
MsgType: m.MsgType,
Format: "org.matrix.custom.html",
FormattedBody: text,
Commits: commits,
}, nil
}

// Create implements PayloadConvertor Create method
func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
// Create implements payloadConvertor Create method
func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) {
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Delete composes Matrix payload for delete a branch or tag.
func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) {
refName := git.RefName(p.Ref).ShortName()
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Fork composes Matrix payload for forked by a repository.
func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) {
baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Issue implements PayloadConvertor Issue method
func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
// Issue implements payloadConvertor Issue method
func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) {
text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// IssueComment implements PayloadConvertor IssueComment method
func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
// IssueComment implements payloadConvertor IssueComment method
func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) {
text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Wiki implements PayloadConvertor Wiki method
func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
// Wiki implements payloadConvertor Wiki method
func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) {
text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Release implements PayloadConvertor Release method
func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
// Release implements payloadConvertor Release method
func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) {
text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Push implements PayloadConvertor Push method
func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
// Push implements payloadConvertor Push method
func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
var commitDesc string

if p.TotalCommits == 1 {
Expand All @@ -149,55 +168,55 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
}

repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)

// for each commit, generate a new line text
for i, commit := range p.Commits {
text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
// add linebreak to each commit but the last
if i < len(p.Commits)-1 {
text += "<br>"
}

}

return getMatrixPayload(text, p.Commits, m.MsgType), nil
return m.newPayload(text, p.Commits...)
}

// PullRequest implements PayloadConvertor PullRequest method
func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
// PullRequest implements payloadConvertor PullRequest method
func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) {
text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Review implements PayloadConvertor Review method
func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
// Review implements payloadConvertor Review method
func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) {
senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title)
repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string

switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return MatrixPayload{}, err
}

text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
}

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

// Repository implements PayloadConvertor Repository method
func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
// Repository implements payloadConvertor Repository method
func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) {
senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string

switch p.Action {
Expand All @@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
case api.HookRepoDeleted:
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
}

return getMatrixPayload(text, nil, m.MsgType), nil
return m.newPayload(text)
}

func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name)
func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name)
var text string

switch p.Action {
Expand All @@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
}

return getMatrixPayload(text, nil, m.MsgType), nil
}

// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(MatrixPayload)

matrix := &MatrixMeta{}
if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
return s, errors.New("GetMatrixPayload meta json:" + err.Error())
}

s.MsgType = messageTypeText[matrix.MessageType]

return convertPayloader(s, p, event)
}

func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
p := MatrixPayload{}
p.FormattedBody = text
p.Body = getMessageBody(text)
p.Format = "org.matrix.custom.html"
p.MsgType = msgType
p.Commits = commits
return &p
return m.newPayload(text)
}

var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
Expand All @@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) {

return hex.EncodeToString(h.Sum(nil)), nil
}

// MatrixLinkToRef Matrix-formatter link to a repo ref
func MatrixLinkToRef(repoURL, ref string) string {
refName := git.RefName(ref).ShortName()
switch {
case strings.HasPrefix(ref, git.BranchPrefix):
return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
case strings.HasPrefix(ref, git.TagPrefix):
return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
default:
return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
}
}
165 changes: 82 additions & 83 deletions services/webhook/matrix_test.go

Large diffs are not rendered by default.

58 changes: 26 additions & 32 deletions services/webhook/msteams.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
package webhook

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
Expand Down Expand Up @@ -56,19 +58,8 @@ type (
}
)

// JSONPayload Marshals the MSTeamsPayload to json
func (m *MSTeamsPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

var _ PayloadConvertor = &MSTeamsPayload{}

// Create implements PayloadConvertor Create method
func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
Expand All @@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}

// Delete implements PayloadConvertor Delete method
func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
// deleted tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
Expand All @@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}

// Fork implements PayloadConvertor Fork method
func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)

return createMSTeamsPayload(
Expand All @@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
}

// Push implements PayloadConvertor Push method
func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
Expand Down Expand Up @@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}

// Issue implements PayloadConvertor Issue method
func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
}

// IssueComment implements PayloadConvertor IssueComment method
func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader
}

// PullRequest implements PayloadConvertor PullRequest method
func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}

// Review implements PayloadConvertor Review method
func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) {
var text, title string
var color int
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return MSTeamsPayload{}, err
}

title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
Expand Down Expand Up @@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.
}

// Repository implements PayloadConvertor Repository method
func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
var title, url string
var color int
switch p.Action {
Expand All @@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
}

// Wiki implements PayloadConvertor Wiki method
func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
}

// Release implements PayloadConvertor Release method
func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
), nil
}

func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)

return createMSTeamsPayload(
Expand All @@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
), nil
}

// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(MSTeamsPayload), p, event)
}

func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload {
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2)
if r != nil {
facts = append(facts, MSTeamsFact{
Expand All @@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
facts = append(facts, *fact)
}

return &MSTeamsPayload{
return MSTeamsPayload{
Type: "MessageCard",
Context: "https://schema.org/extensions",
ThemeColor: fmt.Sprintf("%x", color),
Expand Down Expand Up @@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
},
}
}

type msteamsConvertor struct{}

var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}

func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
return newJSONRequest(msteamsConvertor{}, w, t, true)
}
467 changes: 224 additions & 243 deletions services/webhook/msteams_test.go

Large diffs are not rendered by default.

91 changes: 47 additions & 44 deletions services/webhook/packagist.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package webhook

import (
"errors"
"context"
"fmt"
"net/http"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
Expand Down Expand Up @@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
return s
}

// JSONPayload Marshals the PackagistPayload to json
func (f *PackagistPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(f, "", " ")
if err != nil {
return []byte{}, err
}
return data, nil
}

var _ PayloadConvertor = &PackagistPayload{}

// Create implements PayloadConvertor Create method
func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Delete implements PayloadConvertor Delete method
func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Fork implements PayloadConvertor Fork method
func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Push implements PayloadConvertor Push method
func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) {
return f, nil
// https://packagist.org/about
func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) {
return PackagistPayload{
PackagistRepository: struct {
URL string `json:"url"`
}{
URL: pc.PackageURL,
},
}, nil
}

// Issue implements PayloadConvertor Issue method
func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// IssueComment implements PayloadConvertor IssueComment method
func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// PullRequest implements PayloadConvertor PullRequest method
func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Review implements PayloadConvertor Review method
func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Repository implements PayloadConvertor Repository method
func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Wiki implements PayloadConvertor Wiki method
func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

// Release implements PayloadConvertor Release method
func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) {
return nil, nil
func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}

func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
return nil, nil
type packagistConvertor struct {
PackageURL string
}

// GetPackagistPayload converts a packagist webhook into a PackagistPayload
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(PackagistPayload)
var _ payloadConvertor[PackagistPayload] = packagistConvertor{}

packagist := &PackagistMeta{}
if err := json.Unmarshal([]byte(meta), &packagist); err != nil {
return s, errors.New("GetPackagistPayload meta json:" + err.Error())
func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &PackagistMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err)
}
pc := packagistConvertor{
PackageURL: meta.PackageURL,
}
s.PackagistRepository.URL = packagist.PackageURL
return convertPayloader(s, p, event)
return newJSONRequest(pc, w, t, true)
}
153 changes: 100 additions & 53 deletions services/webhook/packagist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package webhook

import (
"context"
"testing"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"

Expand All @@ -14,155 +17,199 @@ import (
)

func TestPackagistPayload(t *testing.T) {
pc := packagistConvertor{
PackageURL: "https://packagist.org/packages/example",
}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()

d := new(PackagistPayload)
pl, err := d.Create(p)
pl, err := pc.Create(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()

d := new(PackagistPayload)
pl, err := d.Delete(p)
pl, err := pc.Delete(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()

d := new(PackagistPayload)
pl, err := d.Fork(p)
pl, err := pc.Fork(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Push", func(t *testing.T) {
p := pushTestPayload()

d := new(PackagistPayload)
d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN"
pl, err := d.Push(p)
pl, err := pc.Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &PackagistPayload{}, pl)

assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL)
assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL)
})

t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()

d := new(PackagistPayload)
p.Action = api.HookIssueOpened
pl, err := d.Issue(p)
pl, err := pc.Issue(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})

p.Action = api.HookIssueClosed
pl, err = d.Issue(p)
pl, err = pc.Issue(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()

d := new(PackagistPayload)
pl, err := d.IssueComment(p)
pl, err := pc.IssueComment(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()

d := new(PackagistPayload)
pl, err := d.PullRequest(p)
pl, err := pc.PullRequest(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()

d := new(PackagistPayload)
pl, err := d.IssueComment(p)
pl, err := pc.IssueComment(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed

d := new(PackagistPayload)
pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()

d := new(PackagistPayload)
pl, err := d.Repository(p)
pl, err := pc.Repository(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Package", func(t *testing.T) {
p := packageTestPayload()

d := new(PackagistPayload)
pl, err := d.Package(p)
pl, err := pc.Package(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()

d := new(PackagistPayload)
p.Action = api.HookWikiCreated
pl, err := d.Wiki(p)
pl, err := pc.Wiki(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})

p.Action = api.HookWikiEdited
pl, err = d.Wiki(p)
pl, err = pc.Wiki(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})

p.Action = api.HookWikiDeleted
pl, err = d.Wiki(p)
pl, err = pc.Wiki(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})

t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()

d := new(PackagistPayload)
pl, err := d.Release(p)
pl, err := pc.Release(p)
require.NoError(t, err)
require.Nil(t, pl)
require.Equal(t, pl, PackagistPayload{})
})
}

func TestPackagistJSONPayload(t *testing.T) {
p := pushTestPayload()
data, err := p.JSONPayload()
require.NoError(t, err)

hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.PACKAGIST,
URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
Meta: `{"package_url":"https://packagist.org/packages/example"}`,
HTTPMethod: "POST",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
PayloadContent: string(data),
PayloadVersion: 2,
}

req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)

pl, err := new(PackagistPayload).Push(p)
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body PackagistPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL)
}

func TestPackagistEmptyPayload(t *testing.T) {
p := createTestPayload()
data, err := p.JSONPayload()
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &PackagistPayload{}, pl)

json, err := pl.JSONPayload()
hook := &webhook_model.Webhook{
RepoID: 3,
IsActive: true,
Type: webhook_module.PACKAGIST,
URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
Meta: `{"package_url":"https://packagist.org/packages/example"}`,
HTTPMethod: "POST",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventCreate,
PayloadContent: string(data),
PayloadVersion: 2,
}

req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
assert.NotEmpty(t, json)

assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body PackagistPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "", body.PackagistRepository.URL)
}
112 changes: 79 additions & 33 deletions services/webhook/payloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,104 @@
package webhook

import (
"bytes"
"fmt"
"net/http"

webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)

// PayloadConvertor defines the interface to convert system webhook payload to external payload
type PayloadConvertor interface {
api.Payloader
Create(*api.CreatePayload) (api.Payloader, error)
Delete(*api.DeletePayload) (api.Payloader, error)
Fork(*api.ForkPayload) (api.Payloader, error)
Issue(*api.IssuePayload) (api.Payloader, error)
IssueComment(*api.IssueCommentPayload) (api.Payloader, error)
Push(*api.PushPayload) (api.Payloader, error)
PullRequest(*api.PullRequestPayload) (api.Payloader, error)
Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error)
Repository(*api.RepositoryPayload) (api.Payloader, error)
Release(*api.ReleasePayload) (api.Payloader, error)
Wiki(*api.WikiPayload) (api.Payloader, error)
Package(*api.PackagePayload) (api.Payloader, error)
// payloadConvertor defines the interface to convert system payload to webhook payload
type payloadConvertor[T any] interface {
Create(*api.CreatePayload) (T, error)
Delete(*api.DeletePayload) (T, error)
Fork(*api.ForkPayload) (T, error)
Issue(*api.IssuePayload) (T, error)
IssueComment(*api.IssueCommentPayload) (T, error)
Push(*api.PushPayload) (T, error)
PullRequest(*api.PullRequestPayload) (T, error)
Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
Repository(*api.RepositoryPayload) (T, error)
Release(*api.ReleasePayload) (T, error)
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
}

func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
var p P
if err := json.Unmarshal(data, &p); err != nil {
var t T
return t, fmt.Errorf("could not unmarshal payload: %w", err)
}
return convert(p)
}

func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
switch event {
case webhook_module.HookEventCreate:
return s.Create(p.(*api.CreatePayload))
return convertUnmarshalledJSON(rc.Create, data)
case webhook_module.HookEventDelete:
return s.Delete(p.(*api.DeletePayload))
return convertUnmarshalledJSON(rc.Delete, data)
case webhook_module.HookEventFork:
return s.Fork(p.(*api.ForkPayload))
return convertUnmarshalledJSON(rc.Fork, data)
case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
return s.Issue(p.(*api.IssuePayload))
return convertUnmarshalledJSON(rc.Issue, data)
case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
pl, ok := p.(*api.IssueCommentPayload)
if ok {
return s.IssueComment(pl)
}
return s.PullRequest(p.(*api.PullRequestPayload))
// previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
// however I couldn't find in notifier.go such a payload with an HookEvent***Comment event

// History (most recent first):
// - refactored in https://github.com/go-gitea/gitea/pull/12310
// - assertion added in https://github.com/go-gitea/gitea/pull/12046
// - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
// > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload

// In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
return convertUnmarshalledJSON(rc.IssueComment, data)
case webhook_module.HookEventPush:
return s.Push(p.(*api.PushPayload))
return convertUnmarshalledJSON(rc.Push, data)
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
return s.PullRequest(p.(*api.PullRequestPayload))
return convertUnmarshalledJSON(rc.PullRequest, data)
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
return s.Review(p.(*api.PullRequestPayload), event)
return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
return rc.Review(p, event)
}, data)
case webhook_module.HookEventRepository:
return s.Repository(p.(*api.RepositoryPayload))
return convertUnmarshalledJSON(rc.Repository, data)
case webhook_module.HookEventRelease:
return s.Release(p.(*api.ReleasePayload))
return convertUnmarshalledJSON(rc.Release, data)
case webhook_module.HookEventWiki:
return s.Wiki(p.(*api.WikiPayload))
return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
return s.Package(p.(*api.PackagePayload))
return convertUnmarshalledJSON(rc.Package, data)
}
var t T
return t, fmt.Errorf("newPayload unsupported event: %s", event)
}

func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType)
if err != nil {
return nil, nil, err
}

body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
}

req, err := http.NewRequest(w.HTTPMethod, w.URL, bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")

if withDefaultHeaders {
return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
}
return s, nil
return req, body, nil
}
95 changes: 45 additions & 50 deletions services/webhook/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
package webhook

import (
"errors"
"context"
"fmt"
"net/http"
"regexp"
"strings"

Expand Down Expand Up @@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
type SlackPayload struct {
Channel string `json:"channel"`
Text string `json:"text"`
Color string `json:"-"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
UnfurlLinks int `json:"unfurl_links"`
Expand All @@ -56,15 +56,6 @@ type SlackAttachment struct {
Text string `json:"text"`
}

// JSONPayload Marshals the SlackPayload to json
func (s *SlackPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(s, "", " ")
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 {
Expand Down Expand Up @@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string {
return SlackLinkFormatter(url, refName)
}

var _ PayloadConvertor = &SlackPayload{}

// Create implements PayloadConvertor Create method
func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
// Create implements payloadConvertor Create method
func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
Expand All @@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}

// Delete composes Slack payload for delete a branch or tag.
func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) {
refName := git.RefName(p.Ref).ShortName()
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
Expand All @@ -119,16 +108,16 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}

// Fork composes Slack payload for forked by a repository.
func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
func (s slackConvertor) Fork(p *api.ForkPayload) (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 s.createPayload(text, nil), nil
}

// Issue implements PayloadConvertor Issue method
func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
// Issue implements payloadConvertor Issue method
func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) {
text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true)

var attachments []SlackAttachment
Expand All @@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
return s.createPayload(text, attachments), nil
}

// IssueComment implements PayloadConvertor IssueComment method
func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
// IssueComment implements payloadConvertor IssueComment method
func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) {
text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true)

return s.createPayload(text, []SlackAttachment{{
Expand All @@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader,
}}), nil
}

// Wiki implements PayloadConvertor Wiki method
func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
// Wiki implements payloadConvertor Wiki method
func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) {
text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true)

return s.createPayload(text, nil), nil
}

// Release implements PayloadConvertor Release method
func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
// Release implements payloadConvertor Release method
func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) {
text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)

return s.createPayload(text, nil), nil
}

func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)

return s.createPayload(text, nil), nil
}

// Push implements PayloadConvertor Push method
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
// Push implements payloadConvertor Push method
func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
// n new commits
var (
commitDesc string
Expand Down Expand Up @@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}}), nil
}

// PullRequest implements PayloadConvertor PullRequest method
func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
// PullRequest implements payloadConvertor PullRequest method
func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) {
text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true)

var attachments []SlackAttachment
Expand All @@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
return s.createPayload(text, attachments), nil
}

// Review implements PayloadConvertor Review method
func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
// Review implements payloadConvertor Review method
func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
Expand All @@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
return SlackPayload{}, err
}

text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
Expand All @@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
return s.createPayload(text, nil), nil
}

// Repository implements PayloadConvertor Repository method
func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
// Repository implements payloadConvertor Repository method
func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
Expand All @@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro
return s.createPayload(text, nil), nil
}

func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload {
return &SlackPayload{
func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
return SlackPayload{
Channel: s.Channel,
Text: text,
Username: s.Username,
Expand All @@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment)
}
}

// GetSlackPayload converts a slack webhook into a SlackPayload
func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(SlackPayload)

slack := &SlackMeta{}
if err := json.Unmarshal([]byte(meta), &slack); err != nil {
return s, errors.New("GetSlackPayload meta json:" + err.Error())
}
type slackConvertor struct {
Channel string
Username string
IconURL string
Color string
}

s.Channel = slack.Channel
s.Username = slack.Username
s.IconURL = slack.IconURL
s.Color = slack.Color
var _ payloadConvertor[SlackPayload] = slackConvertor{}

return convertPayloader(s, p, event)
func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &SlackMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err)
}
sc := slackConvertor{
Channel: meta.Channel,
Username: meta.Username,
IconURL: meta.IconURL,
Color: meta.Color,
}
return newJSONRequest(sc, w, t, true)
}

var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
Expand Down
Loading