初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/hostmatcher"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/proxy"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
||||
switch w.HTTPMethod {
|
||||
case "", http.MethodPost:
|
||||
switch w.ContentType {
|
||||
case webhook_model.ContentTypeJSON:
|
||||
req, err = http.NewRequest(http.MethodPost, w.URL, strings.NewReader(t.PayloadContent))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
case webhook_model.ContentTypeForm:
|
||||
forms := url.Values{
|
||||
"payload": []string{t.PayloadContent},
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, w.URL, strings.NewReader(forms.Encode()))
|
||||
if err != nil {
|
||||
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 nil, nil, fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
vals := u.Query()
|
||||
vals["payload"] = []string{t.PayloadContent}
|
||||
u.RawQuery = vals.Encode()
|
||||
req, err = http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case http.MethodPut:
|
||||
switch w.Type {
|
||||
case webhook_module.MATRIX: // used when t.Version == 1
|
||||
txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
|
||||
req, err = http.NewRequest(http.MethodPut, url, strings.NewReader(t.PayloadContent))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
||||
}
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
||||
}
|
||||
|
||||
body = []byte(t.PayloadContent)
|
||||
return req, body, addDefaultHeaders(req, []byte(w.Secret), w, t, body)
|
||||
}
|
||||
|
||||
func addDefaultHeaders(req *http.Request, secret []byte, w *webhook_model.Webhook, t *webhook_model.HookTask, payloadContent []byte) error {
|
||||
var signatureSHA1 string
|
||||
var signatureSHA256 string
|
||||
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 {
|
||||
// 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))
|
||||
}
|
||||
|
||||
event := t.EventType.Event()
|
||||
eventType := string(t.EventType)
|
||||
targetType := "default"
|
||||
if w.IsSystemWebhook {
|
||||
targetType = "system"
|
||||
} else if w.RepoID != 0 {
|
||||
targetType = "repository"
|
||||
} else if w.OwnerID != 0 {
|
||||
owner, err := user_model.GetUserByID(req.Context(), w.OwnerID)
|
||||
if owner != nil && err == nil {
|
||||
if owner.IsOrganization() {
|
||||
targetType = "organization"
|
||||
} else {
|
||||
targetType = "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Add("X-Gitea-Delivery", t.UUID)
|
||||
req.Header.Add("X-Gitea-Event", event)
|
||||
req.Header.Add("X-Gitea-Event-Type", eventType)
|
||||
req.Header.Add("X-Gitea-Signature", signatureSHA256)
|
||||
req.Header.Add("X-Gitea-Hook-Installation-Target-Type", targetType)
|
||||
req.Header.Add("X-Gogs-Delivery", t.UUID)
|
||||
req.Header.Add("X-Gogs-Event", event)
|
||||
req.Header.Add("X-Gogs-Event-Type", eventType)
|
||||
req.Header.Add("X-Gogs-Signature", signatureSHA256)
|
||||
req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
|
||||
req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
|
||||
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
|
||||
req.Header["X-GitHub-Event"] = []string{event}
|
||||
req.Header["X-GitHub-Event-Type"] = []string{eventType}
|
||||
req.Header["X-GitHub-Hook-Installation-Target-Type"] = []string{targetType}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
|
||||
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{},
|
||||
}
|
||||
|
||||
// OK We're now ready to attempt to deliver the task - we must double check that it
|
||||
// has not been delivered in the meantime
|
||||
updated, err := webhook_model.MarkTaskDelivered(ctx, t)
|
||||
if err != nil {
|
||||
log.Error("MarkTaskDelivered[%d]: %v", t.ID, err)
|
||||
return fmt.Errorf("unable to mark task[%d] delivered in the db: %w", t.ID, err)
|
||||
}
|
||||
if !updated {
|
||||
// This webhook task has already been attempted to be delivered or is in the process of being delivered
|
||||
log.Trace("Webhook Task[%d] already delivered", t.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// All code from this point will update the hook task
|
||||
defer func() {
|
||||
t.Delivered = timeutil.TimeStampNanoNow()
|
||||
if t.IsSucceed {
|
||||
log.Trace("Hook delivered: %s", t.UUID)
|
||||
} else if !w.IsActive {
|
||||
log.Trace("Hook delivery skipped as webhook is inactive: %s", t.UUID)
|
||||
} else {
|
||||
log.Trace("Hook delivery failed: %s", t.UUID)
|
||||
}
|
||||
|
||||
if err := webhook_model.UpdateHookTask(ctx, t); err != nil {
|
||||
log.Error("UpdateHookTask [%d]: %v", t.ID, err)
|
||||
}
|
||||
|
||||
// Update webhook last delivery status.
|
||||
if t.IsSucceed {
|
||||
w.LastStatus = webhook_module.HookStatusSucceed
|
||||
} else {
|
||||
w.LastStatus = webhook_module.HookStatusFail
|
||||
}
|
||||
if err = webhook_model.UpdateWebhookLastStatus(ctx, w); err != nil {
|
||||
log.Error("UpdateWebhookLastStatus: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if setting.DisableWebhooks {
|
||||
return fmt.Errorf("webhook task skipped (webhooks disabled): [%d]", t.ID)
|
||||
}
|
||||
|
||||
if !w.IsActive {
|
||||
log.Trace("Webhook %s in Webhook Task[%d] is not active", w.URL, t.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := webhookHTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
|
||||
return fmt.Errorf("unable to deliver webhook task[%d] in %s due to error in http client: %w", t.ID, w.URL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Status code is 20x can be seen as succeed.
|
||||
t.IsSucceed = resp.StatusCode/100 == 2
|
||||
t.ResponseInfo.Status = resp.StatusCode
|
||||
for k, vals := range resp.Header {
|
||||
t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
|
||||
}
|
||||
|
||||
p, err := util.ReadWithLimit(resp.Body, 1024*1024)
|
||||
if err != nil {
|
||||
t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
|
||||
return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err)
|
||||
}
|
||||
t.ResponseInfo.Body = string(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
webhookHTTPClient *http.Client
|
||||
once sync.Once
|
||||
hostMatchers []glob.Glob
|
||||
)
|
||||
|
||||
func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
|
||||
if setting.Webhook.ProxyURL == "" {
|
||||
return proxy.Proxy()
|
||||
}
|
||||
|
||||
once.Do(func() {
|
||||
for _, h := range setting.Webhook.ProxyHosts {
|
||||
if g, err := glob.Compile(h); err == nil {
|
||||
hostMatchers = append(hostMatchers, g)
|
||||
} else {
|
||||
log.Error("glob.Compile %s failed: %v", h, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return func(req *http.Request) (*url.URL, error) {
|
||||
for _, v := range hostMatchers {
|
||||
if v.Match(req.URL.Host) {
|
||||
if !allowList.MatchHostName(req.URL.Host) {
|
||||
return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
|
||||
}
|
||||
return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
|
||||
}
|
||||
}
|
||||
return http.ProxyFromEnvironment(req)
|
||||
}
|
||||
}
|
||||
|
||||
// Init starts the hooks delivery thread
|
||||
func Init() error {
|
||||
timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
|
||||
|
||||
allowedHostListValue := setting.Webhook.AllowedHostList
|
||||
if allowedHostListValue == "" {
|
||||
allowedHostListValue = hostmatcher.MatchBuiltinExternal
|
||||
}
|
||||
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", allowedHostListValue)
|
||||
|
||||
webhookHTTPClient = &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
|
||||
Proxy: webhookProxy(allowedHostMatcher),
|
||||
DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
|
||||
},
|
||||
}
|
||||
|
||||
hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler)
|
||||
if hookQueue == nil {
|
||||
return errors.New("unable to create webhook_sender queue")
|
||||
}
|
||||
go graceful.GetManager().RunWithCancel(hookQueue)
|
||||
|
||||
go graceful.GetManager().RunWithShutdownContext(populateWebhookSendingQueue)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateWebhookSendingQueue(ctx context.Context) {
|
||||
ctx, _, finished := process.GetManager().AddContext(ctx, "Webhook: Populate sending queue")
|
||||
defer finished()
|
||||
|
||||
lowerID := int64(0)
|
||||
for {
|
||||
taskIDs, err := webhook_model.FindUndeliveredHookTaskIDs(ctx, lowerID)
|
||||
if err != nil {
|
||||
log.Error("Unable to populate webhook queue as FindUndeliveredHookTaskIDs failed: %v", err)
|
||||
return
|
||||
}
|
||||
if len(taskIDs) == 0 {
|
||||
return
|
||||
}
|
||||
lowerID = taskIDs[len(taskIDs)-1]
|
||||
|
||||
for _, taskID := range taskIDs {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Warn("Shutdown before Webhook Sending queue finishing being populated")
|
||||
return
|
||||
default:
|
||||
}
|
||||
if err := enqueueHookTask(taskID); err != nil {
|
||||
log.Error("Unable to push HookTask[%d] to the Webhook Sending queue: %v", taskID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/hostmatcher"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebhookProxy(t *testing.T) {
|
||||
oldWebhook := setting.Webhook
|
||||
t.Cleanup(func() {
|
||||
setting.Webhook = oldWebhook
|
||||
})
|
||||
|
||||
setting.Webhook.ProxyURL = "http://localhost:8080"
|
||||
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
|
||||
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
|
||||
|
||||
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
|
||||
|
||||
tests := []struct {
|
||||
req string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
|
||||
want: "http://localhost:8080",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://s.discordapp.com/assets/xxxxxx",
|
||||
want: "http://localhost:8080",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://github.com/a/b",
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://www.discordapp.com/assets/xxxxxx",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.req, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, tt.req, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
u, err := webhookProxy(allowedHostMatcher)(req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
got := ""
|
||||
if u != nil {
|
||||
got = u.String()
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookDeliverAuthorizationHeader(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, "/webhook", r.URL.Path)
|
||||
assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
done <- struct{}{}
|
||||
}))
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
URL: s.URL + "/webhook",
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
IsActive: true,
|
||||
Type: webhook_module.GITEA,
|
||||
}
|
||||
err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook))
|
||||
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
hookTask, err = webhook_model.CreateHookTask(t.Context(), hookTask)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hookTask)
|
||||
|
||||
assert.NoError(t, Deliver(t.Context(), hookTask))
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("waited to long for request to happen")
|
||||
}
|
||||
|
||||
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)
|
||||
version2Body := `{
|
||||
"body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1",
|
||||
"msgtype": "",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1",
|
||||
"io.gitea.commits": [
|
||||
{
|
||||
"id": "2020558fe2e34debb818a514715839cabd25e778",
|
||||
"message": "commit message",
|
||||
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
|
||||
"author": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"committer": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"verification": null,
|
||||
"timestamp": "0001-01-01T00:00:00Z",
|
||||
"added": null,
|
||||
"removed": null,
|
||||
"modified": null
|
||||
},
|
||||
{
|
||||
"id": "2020558fe2e34debb818a514715839cabd25e778",
|
||||
"message": "commit message",
|
||||
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
|
||||
"author": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"committer": {
|
||||
"name": "user1",
|
||||
"email": "user1@localhost",
|
||||
"username": "user1"
|
||||
},
|
||||
"verification": null,
|
||||
"timestamp": "0001-01-01T00:00:00Z",
|
||||
"added": null,
|
||||
"removed": null,
|
||||
"modified": null
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
testVersion := 0
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "PUT", r.Method)
|
||||
assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/"))
|
||||
assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash
|
||||
switch testVersion {
|
||||
case 1: // Version 1
|
||||
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
|
||||
assert.Empty(t, r.Header.Get("Content-Type"))
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{"data": 42}`, string(body))
|
||||
|
||||
case 2: // 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.JSONEq(t, version2Body, string(body))
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
t.Fatalf("unexpected url path %s", r.URL.Path)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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(t.Context(), hook))
|
||||
|
||||
t.Run("Version 1", func(t *testing.T) {
|
||||
testVersion = 1
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: `{"data": 42}`,
|
||||
PayloadVersion: 1,
|
||||
}
|
||||
|
||||
hookTask, err := webhook_model.CreateHookTask(t.Context(), hookTask)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hookTask)
|
||||
|
||||
assert.NoError(t, Deliver(t.Context(), 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)
|
||||
|
||||
testVersion = 2
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
hookTask, err = webhook_model.CreateHookTask(t.Context(), hookTask)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hookTask)
|
||||
|
||||
assert.NoError(t, Deliver(t.Context(), 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
|
||||
httpMethod string // default to POST
|
||||
}
|
||||
|
||||
cases := map[string]*hookCase{
|
||||
webhook_module.SLACK: {},
|
||||
webhook_module.DISCORD: {},
|
||||
webhook_module.DINGTALK: {},
|
||||
webhook_module.TELEGRAM: {},
|
||||
webhook_module.MSTEAMS: {},
|
||||
webhook_module.FEISHU: {},
|
||||
webhook_module.MATRIX: {httpMethod: "PUT"},
|
||||
webhook_module.WECHATWORK: {},
|
||||
webhook_module.PACKAGIST: {},
|
||||
}
|
||||
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path"
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
|
||||
assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path)
|
||||
body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan
|
||||
cases[typ].gotBody <- body
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
assert.NoError(t, err)
|
||||
|
||||
for typ := range cases {
|
||||
cases[typ].gotBody = make(chan []byte, 1)
|
||||
t.Run(typ, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: typ,
|
||||
URL: s.URL + "/" + typ,
|
||||
Meta: "{}",
|
||||
}
|
||||
assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook))
|
||||
|
||||
hookTask := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
hookTask, err := webhook_model.CreateHookTask(t.Context(), hookTask)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hookTask)
|
||||
|
||||
assert.NoError(t, Deliver(t.Context(), hookTask))
|
||||
|
||||
select {
|
||||
case gotBody := <-cases[typ].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), "delivered webhook payload doesn't match saved request")
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("waited to long for request to happen")
|
||||
}
|
||||
|
||||
assert.True(t, hookTask.IsSucceed)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
dingtalk "gitea.com/lunny/dingtalk_webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
DingtalkPayload dingtalk.Payload
|
||||
dingtalkConvertor struct{}
|
||||
)
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
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)
|
||||
|
||||
return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
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)
|
||||
|
||||
return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
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, "view forked repo "+p.Repo.FullName, p.Repo.HTMLURL), nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
|
||||
var (
|
||||
branchName = git.RefName(p.Ref).ShortName()
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
var titleLink, linkText string
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 new commit"
|
||||
titleLink = p.Commits[0].URL
|
||||
linkText = "view commit"
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
||||
titleLink = p.CompareURL
|
||||
linkText = "view commits"
|
||||
}
|
||||
if titleLink == "" {
|
||||
titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
if commit.Author != nil {
|
||||
authorName = " - " + commit.Author.Name
|
||||
}
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
return createDingtalkPayload(title, text.String(), linkText, titleLink), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
|
||||
text, issueTitle, extraMarkdown, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+extraMarkdown, "view issue", p.Issue.HTMLURL), nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
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 (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 (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
|
||||
text, issueTitle, extraMarkdown, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+extraMarkdown, "view pull request", p.PullRequest.HTMLURL), nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
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 DingtalkPayload{}, err
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
||||
text = p.Review.Content
|
||||
}
|
||||
|
||||
return createDingtalkPayload(title, title+"\r\n\r\n"+text, "view pull request", p.PullRequest.HTMLURL), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
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{
|
||||
MsgType: "text",
|
||||
Text: struct {
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
Content: title,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return DingtalkPayload{}, nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
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 (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
||||
}
|
||||
|
||||
func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
|
||||
text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
|
||||
}
|
||||
|
||||
func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
|
||||
}
|
||||
|
||||
func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "Workflow Job", p.WorkflowJob.HTMLURL), nil
|
||||
}
|
||||
|
||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
|
||||
return DingtalkPayload{
|
||||
MsgType: "actionCard",
|
||||
ActionCard: dingtalk.ActionCard{
|
||||
Text: strings.TrimSpace(text),
|
||||
Title: strings.TrimSpace(title),
|
||||
HideAvatar: "0",
|
||||
SingleTitle: singleTitle,
|
||||
|
||||
// https://developers.dingtalk.com/document/app/message-link-description
|
||||
// to open the link in browser, we should use this URL, otherwise the page is displayed inside DingTalk client, very difficult to visit non-public URLs.
|
||||
SingleURL: "dingtalk://dingtalkclient/page/link?pc_slide=false&url=" + url.QueryEscape(singleURL),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDingTalkPayload(t *testing.T) {
|
||||
parseRealSingleURL := func(singleURL string) string {
|
||||
if u, err := url.Parse(singleURL); err == nil {
|
||||
assert.Equal(t, "dingtalk", u.Scheme)
|
||||
assert.Equal(t, "dingtalkclient", u.Host)
|
||||
assert.Equal(t, "/page/link", u.Path)
|
||||
return u.Query().Get("url")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
dc := dingtalkConvertor{}
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := dc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := dc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := dc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text)
|
||||
assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[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.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view commits", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := dc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text)
|
||||
assert.Equal(t, "#2 crash", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = dc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "#2 crash", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := dc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text)
|
||||
assert.Equal(t, "#2 crash", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := dc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text)
|
||||
assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := dc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text)
|
||||
assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := dc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view repository", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := dc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view package", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := dc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text)
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title)
|
||||
assert.Equal(t, "view release", pl.ActionCard.SingleTitle)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDingTalkJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.DINGTALK,
|
||||
URL: "https://dingtalk.example.com/",
|
||||
Meta: ``,
|
||||
HTTPMethod: "POST",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newDingtalkRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://dingtalk.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 DingtalkPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[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.ActionCard.Text)
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// DiscordEmbedFooter for Embed Footer Structure.
|
||||
DiscordEmbedFooter struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// DiscordEmbedAuthor for Embed Author Structure
|
||||
DiscordEmbedAuthor struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
IconURL string `json:"icon_url"`
|
||||
}
|
||||
|
||||
// DiscordEmbedField for Embed Field Structure
|
||||
DiscordEmbedField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// DiscordEmbed is for Embed Structure
|
||||
DiscordEmbed struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
Color int `json:"color"`
|
||||
Footer DiscordEmbedFooter `json:"footer"`
|
||||
Author DiscordEmbedAuthor `json:"author"`
|
||||
Fields []DiscordEmbedField `json:"fields"`
|
||||
}
|
||||
|
||||
// DiscordPayload represents
|
||||
DiscordPayload struct {
|
||||
Wait bool `json:"wait"`
|
||||
Content string `json:"content"`
|
||||
Username string `json:"username,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
TTS bool `json:"tts"`
|
||||
Embeds []DiscordEmbed `json:"embeds"`
|
||||
}
|
||||
|
||||
// DiscordMeta contains the discord metadata
|
||||
DiscordMeta struct {
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetDiscordHook returns discord metadata
|
||||
func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta {
|
||||
s := &DiscordMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||
log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func color(clr string) int {
|
||||
if clr != "" {
|
||||
clr = strings.TrimLeft(clr, "#")
|
||||
if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
|
||||
return int(s)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
var (
|
||||
greenColor = color("1ac600")
|
||||
greenColorLight = color("bfe5bf")
|
||||
yellowColor = color("ffd930")
|
||||
greyColor = color("4f545c")
|
||||
purpleColor = color("7289da")
|
||||
orangeColor = color("eb6420")
|
||||
orangeColorLight = color("e68d60")
|
||||
redColor = color("ff3232")
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
|
||||
// Discord has some limits in place for the embeds.
|
||||
// According to some tests, there is no consistent limit for different character sets.
|
||||
// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
|
||||
// To keep it simple, we currently truncate at 2000.
|
||||
const discordDescriptionCharactersLimit = 2000
|
||||
|
||||
type discordConvertor struct {
|
||||
Username string
|
||||
AvatarURL string
|
||||
}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
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)
|
||||
|
||||
return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), greenColor), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
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)
|
||||
|
||||
return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), redColor), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
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 discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
|
||||
var (
|
||||
branchName = git.RefName(p.Ref).ShortName()
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
var titleLink string
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 new commit"
|
||||
titleLink = p.Commits[0].URL
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
||||
titleLink = p.CompareURL
|
||||
}
|
||||
if titleLink == "" {
|
||||
titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
// limit the commit message display to just the summary, otherwise it would be hard to read
|
||||
message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r")
|
||||
|
||||
// a limit of 50 is set because GitHub does the same
|
||||
if utf8.RuneCountInString(message) > 50 {
|
||||
message = fmt.Sprintf("%.47s...", message)
|
||||
}
|
||||
fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return d.createPayload(p.Sender, title, text.String(), titleLink, greenColor), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
|
||||
title, _, extraMarkdown, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, title, extraMarkdown, p.Issue.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
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 discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
|
||||
title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, title, extraMarkdown, p.PullRequest.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
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 DiscordPayload{}, err
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
||||
text = p.Review.Content
|
||||
|
||||
switch event {
|
||||
case webhook_module.HookEventPullRequestReviewApproved:
|
||||
color = greenColor
|
||||
case webhook_module.HookEventPullRequestReviewRejected:
|
||||
color = redColor
|
||||
case webhook_module.HookEventPullRequestReviewComment:
|
||||
color = greyColor
|
||||
default:
|
||||
color = yellowColor
|
||||
}
|
||||
}
|
||||
|
||||
return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
|
||||
var title, url string
|
||||
var color int
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
||||
url = p.Repository.HTMLURL
|
||||
color = greenColor
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
color = redColor
|
||||
}
|
||||
|
||||
return d.createPayload(p.Sender, title, "", url, color), nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
|
||||
text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
|
||||
htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
|
||||
|
||||
var description string
|
||||
if p.Action != api.HookWikiDeleted {
|
||||
description = p.Comment
|
||||
}
|
||||
|
||||
return d.createPayload(p.Sender, text, description, htmlLink, color), nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
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 discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
|
||||
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
|
||||
text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
|
||||
text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
|
||||
text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func newDiscordRequest(_ 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)
|
||||
}
|
||||
var pc payloadConvertor[DiscordPayload] = discordConvertor{
|
||||
Username: meta.Username,
|
||||
AvatarURL: meta.IconURL,
|
||||
}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest)
|
||||
}
|
||||
|
||||
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
|
||||
switch event {
|
||||
case webhook_module.HookEventPullRequestReviewApproved:
|
||||
return "approved", nil
|
||||
case webhook_module.HookEventPullRequestReviewRejected:
|
||||
return "requested changes", nil
|
||||
case webhook_module.HookEventPullRequestReviewComment:
|
||||
return "comment", nil
|
||||
default:
|
||||
return "", errors.New("unknown event type")
|
||||
}
|
||||
}
|
||||
|
||||
func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
|
||||
return DiscordPayload{
|
||||
Username: d.Username,
|
||||
AvatarURL: d.AvatarURL,
|
||||
Embeds: []DiscordEmbed{
|
||||
{
|
||||
Title: title,
|
||||
Description: util.TruncateRunes(text, discordDescriptionCharactersLimit),
|
||||
URL: url,
|
||||
Color: color,
|
||||
Author: DiscordEmbedAuthor{
|
||||
Name: s.UserName,
|
||||
URL: setting.AppURL + s.UserName,
|
||||
IconURL: s.AvatarURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDiscordPayload(t *testing.T) {
|
||||
dc := discordConvertor{}
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := dc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := dc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := dc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PushWithMultilineCommitMessage", func(t *testing.T) {
|
||||
p := pushTestMultilineCommitMessagePayload()
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) chore: This is a commit summary - user1", pl.Embeds[0].Description)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PushWithLongCommitSummary", func(t *testing.T) {
|
||||
p := pushTestPayloadWithCommitMessage("This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好 ⚠️⚠️️\n\nThis is the message body")
|
||||
|
||||
pl, err := dc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) This is a commit summary ⚠️⚠️⚠️⚠️ containing 你好... - user1", pl.Embeds[0].Description)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := dc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "issue body", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = dc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := dc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "more info needed", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := dc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := dc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "changes requested", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "good job", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := dc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := dc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = dc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title)
|
||||
assert.Empty(t, pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := dc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pl.Embeds, 1)
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title)
|
||||
assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL)
|
||||
assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
|
||||
assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
|
||||
assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiscordJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.DISCORD,
|
||||
URL: "https://discord.example.com/",
|
||||
Meta: `{}`,
|
||||
HTTPMethod: "POST",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newDiscordRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://discord.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 DiscordPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// FeishuPayload represents the payload for Feishu webhook
|
||||
FeishuPayload struct {
|
||||
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
|
||||
Sign string `json:"sign,omitempty"` // Signature for verification
|
||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
)
|
||||
|
||||
func newFeishuTextPayload(text string) FeishuPayload {
|
||||
return FeishuPayload{
|
||||
MsgType: "text",
|
||||
Content: struct {
|
||||
Text string `json:"text"`
|
||||
}{
|
||||
Text: strings.TrimSpace(text),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type feishuConvertor struct{}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
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)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
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)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
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 (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
|
||||
var (
|
||||
branchName = git.RefName(p.Ref).ShortName()
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
var text strings.Builder
|
||||
fmt.Fprintf(&text, "[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
if commit.Author != nil {
|
||||
authorName = " - " + commit.Author.Name
|
||||
}
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
return newFeishuTextPayload(text.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
|
||||
title, link, by, operator, result, assignees := getIssuesInfo(p)
|
||||
if assignees != "" {
|
||||
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
||||
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
|
||||
}
|
||||
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 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 (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 (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
|
||||
title, link, by, operator, result, assignees := getPullRequestInfo(p)
|
||||
if assignees != "" {
|
||||
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
||||
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
|
||||
}
|
||||
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 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 (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
|
||||
action, err := parseHookPullRequestEventType(event)
|
||||
if err != nil {
|
||||
return FeishuPayload{}, err
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
||||
text := p.Review.Content
|
||||
|
||||
return newFeishuTextPayload(title + "\r\n\r\n" + text), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
|
||||
var text string
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
text = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
||||
return newFeishuTextPayload(text), nil
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
return FeishuPayload{}, nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
|
||||
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
|
||||
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
|
||||
text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// feishuGenSign generates a signature for Feishu webhook
|
||||
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||
func feishuGenSign(secret string, timestamp int64) string {
|
||||
// key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
|
||||
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
||||
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add timestamp and signature if secret is provided
|
||||
if w.Secret != "" {
|
||||
timestamp := time.Now().Unix()
|
||||
payload.Timestamp = timestamp
|
||||
payload.Sign = feishuGenSign(w.Secret, timestamp)
|
||||
}
|
||||
|
||||
return prepareJSONRequest(payload, w, t, false /* no default headers */)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFeishuPayload(t *testing.T) {
|
||||
fc := feishuConvertor{}
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := fc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := fc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := fc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := fc.Push(p)
|
||||
require.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", pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := fc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
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 = fc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
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()
|
||||
|
||||
pl, err := fc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
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()
|
||||
|
||||
pl, err := fc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
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()
|
||||
|
||||
pl, err := fc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
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
|
||||
|
||||
pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
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()
|
||||
|
||||
pl, err := fc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := fc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := fc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = fc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = fc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := fc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFeishuJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.FEISHU,
|
||||
URL: "https://feishu.example.com/",
|
||||
Meta: `{}`,
|
||||
HTTPMethod: "POST",
|
||||
Secret: "secret",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newFeishuRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
|
||||
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)
|
||||
assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
|
||||
|
||||
// a separate sign test, the result is generated by official python code, so the algo must be correct
|
||||
assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type linkFormatter = func(string, string) string
|
||||
|
||||
// noneLinkFormatter does not create a link but just returns the text
|
||||
func noneLinkFormatter(url, text string) string {
|
||||
return text
|
||||
}
|
||||
|
||||
// htmlLinkFormatter creates a HTML link
|
||||
func htmlLinkFormatter(url, text string) string {
|
||||
return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
|
||||
}
|
||||
|
||||
// getPullRequestInfo gets the information for a pull request
|
||||
func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, operateResult, assignees string) {
|
||||
title = fmt.Sprintf("[PullRequest-%s #%d]: %s\n%s", p.Repository.FullName, p.PullRequest.Index, p.Action, p.PullRequest.Title)
|
||||
assignList := p.PullRequest.Assignees
|
||||
assignStringList := make([]string, len(assignList))
|
||||
|
||||
for i, user := range assignList {
|
||||
assignStringList[i] = user.UserName
|
||||
}
|
||||
switch p.Action {
|
||||
case api.HookIssueAssigned:
|
||||
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
|
||||
case api.HookIssueUnassigned:
|
||||
operateResult = p.Sender.UserName + " unassigned this for someone"
|
||||
case api.HookIssueMilestoned:
|
||||
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
|
||||
}
|
||||
link = p.PullRequest.HTMLURL
|
||||
by = "PullRequest by " + p.PullRequest.Poster.UserName
|
||||
if len(assignStringList) > 0 {
|
||||
assignees = "Assignees: " + strings.Join(assignStringList, ", ")
|
||||
}
|
||||
operator = "Operator: " + p.Sender.UserName
|
||||
return title, link, by, operator, operateResult, assignees
|
||||
}
|
||||
|
||||
// getIssuesInfo gets the information for an issue
|
||||
func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operateResult, assignees string) {
|
||||
issueTitle = fmt.Sprintf("[Issue-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
||||
assignList := p.Issue.Assignees
|
||||
assignStringList := make([]string, len(assignList))
|
||||
|
||||
for i, user := range assignList {
|
||||
assignStringList[i] = user.UserName
|
||||
}
|
||||
switch p.Action {
|
||||
case api.HookIssueAssigned:
|
||||
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
|
||||
case api.HookIssueUnassigned:
|
||||
operateResult = p.Sender.UserName + " unassigned this for someone"
|
||||
case api.HookIssueMilestoned:
|
||||
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
|
||||
}
|
||||
link = p.Issue.HTMLURL
|
||||
by = "Issue by " + p.Issue.Poster.UserName
|
||||
if len(assignStringList) > 0 {
|
||||
assignees = "Assignees: " + strings.Join(assignStringList, ", ")
|
||||
}
|
||||
operator = "Operator: " + p.Sender.UserName
|
||||
return issueTitle, link, by, operator, operateResult, assignees
|
||||
}
|
||||
|
||||
// getIssuesCommentInfo gets the information for a comment
|
||||
func getIssuesCommentInfo(p *api.IssueCommentPayload) (title, link, by, operator string) {
|
||||
title = fmt.Sprintf("[Comment-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
||||
link = p.Issue.HTMLURL
|
||||
if p.IsPull {
|
||||
by = "PullRequest by " + p.Issue.Poster.UserName
|
||||
} else {
|
||||
by = "Issue by " + p.Issue.Poster.UserName
|
||||
}
|
||||
operator = "Operator: " + p.Sender.UserName
|
||||
return title, link, by, operator
|
||||
}
|
||||
|
||||
func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, withSender bool) (text, issueTitle, extraMarkdown string, color int) {
|
||||
color = yellowColor
|
||||
issueTitle = fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
|
||||
titleLink := linkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index), issueTitle)
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
|
||||
switch p.Action {
|
||||
case api.HookIssueOpened:
|
||||
text = fmt.Sprintf("[%s] Issue opened: %s", repoLink, titleLink)
|
||||
color = orangeColor
|
||||
case api.HookIssueClosed:
|
||||
text = fmt.Sprintf("[%s] Issue closed: %s", repoLink, titleLink)
|
||||
color = redColor
|
||||
case api.HookIssueReOpened:
|
||||
text = fmt.Sprintf("[%s] Issue re-opened: %s", repoLink, titleLink)
|
||||
case api.HookIssueEdited:
|
||||
text = fmt.Sprintf("[%s] Issue edited: %s", repoLink, titleLink)
|
||||
case api.HookIssueAssigned:
|
||||
list := make([]string, len(p.Issue.Assignees))
|
||||
for i, user := range p.Issue.Assignees {
|
||||
list[i] = linkFormatter(setting.AppURL+url.PathEscape(user.UserName), user.UserName)
|
||||
}
|
||||
text = fmt.Sprintf("[%s] Issue assigned to %s: %s", repoLink, strings.Join(list, ", "), titleLink)
|
||||
color = greenColor
|
||||
case api.HookIssueUnassigned:
|
||||
text = fmt.Sprintf("[%s] Issue unassigned: %s", repoLink, titleLink)
|
||||
case api.HookIssueLabelUpdated:
|
||||
text = fmt.Sprintf("[%s] Issue labels updated: %s", repoLink, titleLink)
|
||||
case api.HookIssueLabelCleared:
|
||||
text = fmt.Sprintf("[%s] Issue labels cleared: %s", repoLink, titleLink)
|
||||
case api.HookIssueSynchronized:
|
||||
text = fmt.Sprintf("[%s] Issue synchronized: %s", repoLink, titleLink)
|
||||
case api.HookIssueMilestoned:
|
||||
mileStoneLink := fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
|
||||
text = fmt.Sprintf("[%s] Issue milestoned to %s: %s", repoLink,
|
||||
linkFormatter(mileStoneLink, p.Issue.Milestone.Title), titleLink)
|
||||
case api.HookIssueDemilestoned:
|
||||
text = fmt.Sprintf("[%s] Issue milestone cleared: %s", repoLink, titleLink)
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
if p.Action == api.HookIssueOpened || p.Action == api.HookIssueEdited {
|
||||
extraMarkdown = p.Issue.Body
|
||||
}
|
||||
|
||||
return text, issueTitle, extraMarkdown, color
|
||||
}
|
||||
|
||||
func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkFormatter, withSender bool) (text, issueTitle, extraMarkdown string, color int) {
|
||||
color = yellowColor
|
||||
issueTitle = fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
|
||||
titleLink := linkFormatter(p.PullRequest.URL, issueTitle)
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
|
||||
switch p.Action {
|
||||
case api.HookIssueOpened:
|
||||
text = fmt.Sprintf("[%s] Pull request opened: %s", repoLink, titleLink)
|
||||
extraMarkdown = p.PullRequest.Body
|
||||
color = greenColor
|
||||
case api.HookIssueClosed:
|
||||
if p.PullRequest.HasMerged {
|
||||
text = fmt.Sprintf("[%s] Pull request merged: %s", repoLink, titleLink)
|
||||
color = purpleColor
|
||||
} else {
|
||||
text = fmt.Sprintf("[%s] Pull request closed: %s", repoLink, titleLink)
|
||||
color = redColor
|
||||
}
|
||||
case api.HookIssueReOpened:
|
||||
text = fmt.Sprintf("[%s] Pull request re-opened: %s", repoLink, titleLink)
|
||||
case api.HookIssueEdited:
|
||||
text = fmt.Sprintf("[%s] Pull request edited: %s", repoLink, titleLink)
|
||||
extraMarkdown = p.PullRequest.Body
|
||||
case api.HookIssueAssigned:
|
||||
list := make([]string, len(p.PullRequest.Assignees))
|
||||
for i, user := range p.PullRequest.Assignees {
|
||||
list[i] = linkFormatter(setting.AppURL+user.UserName, user.UserName)
|
||||
}
|
||||
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s", repoLink,
|
||||
strings.Join(list, ", "), titleLink)
|
||||
color = greenColor
|
||||
case api.HookIssueUnassigned:
|
||||
text = fmt.Sprintf("[%s] Pull request unassigned: %s", repoLink, titleLink)
|
||||
case api.HookIssueLabelUpdated:
|
||||
text = fmt.Sprintf("[%s] Pull request labels updated: %s", repoLink, titleLink)
|
||||
case api.HookIssueLabelCleared:
|
||||
text = fmt.Sprintf("[%s] Pull request labels cleared: %s", repoLink, titleLink)
|
||||
case api.HookIssueSynchronized:
|
||||
text = fmt.Sprintf("[%s] Pull request synchronized: %s", repoLink, titleLink)
|
||||
case api.HookIssueMilestoned:
|
||||
mileStoneLink := fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
|
||||
text = fmt.Sprintf("[%s] Pull request milestoned to %s: %s", repoLink,
|
||||
linkFormatter(mileStoneLink, p.PullRequest.Milestone.Title), titleLink)
|
||||
case api.HookIssueDemilestoned:
|
||||
text = fmt.Sprintf("[%s] Pull request milestone cleared: %s", repoLink, titleLink)
|
||||
case api.HookIssueReviewed:
|
||||
text = fmt.Sprintf("[%s] Pull request reviewed: %s", repoLink, titleLink)
|
||||
extraMarkdown = p.Review.Content
|
||||
case api.HookIssueReviewRequested:
|
||||
text = fmt.Sprintf("[%s] Pull request review requested: %s", repoLink, titleLink)
|
||||
case api.HookIssueReviewRequestRemoved:
|
||||
text = fmt.Sprintf("[%s] Pull request review request removed: %s", repoLink, titleLink)
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, issueTitle, extraMarkdown, color
|
||||
}
|
||||
|
||||
func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
refLink := linkFormatter(p.Repository.HTMLURL+"/releases/tag/"+util.PathEscapeSegments(p.Release.TagName), p.Release.TagName)
|
||||
|
||||
switch p.Action {
|
||||
case api.HookReleasePublished:
|
||||
text = fmt.Sprintf("[%s] Release created: %s", repoLink, refLink)
|
||||
color = greenColor
|
||||
case api.HookReleaseUpdated:
|
||||
text = fmt.Sprintf("[%s] Release updated: %s", repoLink, refLink)
|
||||
color = yellowColor
|
||||
case api.HookReleaseDeleted:
|
||||
text = fmt.Sprintf("[%s] Release deleted: %s", repoLink, refLink)
|
||||
color = redColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getWikiPayloadInfo(p *api.WikiPayload, linkFormatter linkFormatter, withSender bool) (string, int, string) {
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
pageLink := linkFormatter(p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page), p.Page)
|
||||
|
||||
var text string
|
||||
color := greenColor
|
||||
|
||||
switch p.Action {
|
||||
case api.HookWikiCreated:
|
||||
text = fmt.Sprintf("[%s] New wiki page '%s'", repoLink, pageLink)
|
||||
case api.HookWikiEdited:
|
||||
text = fmt.Sprintf("[%s] Wiki page '%s' edited", repoLink, pageLink)
|
||||
color = yellowColor
|
||||
case api.HookWikiDeleted:
|
||||
text = fmt.Sprintf("[%s] Wiki page '%s' deleted", repoLink, pageLink)
|
||||
color = redColor
|
||||
}
|
||||
|
||||
if p.Action != api.HookWikiDeleted && p.Comment != "" {
|
||||
text += fmt.Sprintf(" (%s)", p.Comment)
|
||||
}
|
||||
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color, pageLink
|
||||
}
|
||||
|
||||
func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFormatter, withSender bool) (string, string, int) {
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
issueTitle := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
|
||||
|
||||
var text, typ, titleLink string
|
||||
color := yellowColor
|
||||
|
||||
if p.IsPull {
|
||||
typ = "pull request"
|
||||
titleLink = linkFormatter(p.Comment.PRURL, issueTitle)
|
||||
} else {
|
||||
typ = "issue"
|
||||
titleLink = linkFormatter(p.Comment.IssueURL, issueTitle)
|
||||
}
|
||||
|
||||
switch p.Action {
|
||||
case api.HookIssueCommentCreated:
|
||||
text = fmt.Sprintf("[%s] New comment on %s %s", repoLink, typ, titleLink)
|
||||
if p.IsPull {
|
||||
color = greenColorLight
|
||||
} else {
|
||||
color = orangeColorLight
|
||||
}
|
||||
case api.HookIssueCommentEdited:
|
||||
text = fmt.Sprintf("[%s] Comment edited on %s %s", repoLink, typ, titleLink)
|
||||
case api.HookIssueCommentDeleted:
|
||||
text = fmt.Sprintf("[%s] Comment deleted on %s %s", repoLink, typ, titleLink)
|
||||
color = redColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, issueTitle, color
|
||||
}
|
||||
|
||||
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
|
||||
|
||||
switch p.Action {
|
||||
case api.HookPackageCreated:
|
||||
text = "Package created: " + refLink
|
||||
color = greenColor
|
||||
case api.HookPackageDeleted:
|
||||
text = "Package deleted: " + refLink
|
||||
color = redColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
refLink := linkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
|
||||
|
||||
text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
|
||||
color = greenColor
|
||||
if withSender {
|
||||
if user_model.GetSystemUserByName(p.Sender.UserName) != nil {
|
||||
text += " by " + p.Sender.FullName
|
||||
} else {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
description := p.WorkflowRun.Conclusion
|
||||
if description == "" {
|
||||
description = p.WorkflowRun.Status
|
||||
}
|
||||
refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description)
|
||||
|
||||
text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
|
||||
switch description {
|
||||
case "waiting":
|
||||
color = orangeColor
|
||||
case "queued":
|
||||
color = orangeColorLight
|
||||
case "success":
|
||||
color = greenColor
|
||||
case "failure":
|
||||
color = redColor
|
||||
case "cancelled":
|
||||
color = yellowColor
|
||||
case "skipped":
|
||||
color = purpleColor
|
||||
default:
|
||||
color = greyColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
description := p.WorkflowJob.Conclusion
|
||||
if description == "" {
|
||||
description = p.WorkflowJob.Status
|
||||
}
|
||||
refLink := linkFormatter(p.WorkflowJob.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowJob.Name, p.WorkflowJob.RunID)+"["+base.ShortSha(p.WorkflowJob.HeadSha)+"]:"+description)
|
||||
|
||||
text = fmt.Sprintf("Workflow Job %s: %s", p.Action, refLink)
|
||||
switch description {
|
||||
case "waiting":
|
||||
color = orangeColor
|
||||
case "queued":
|
||||
color = orangeColorLight
|
||||
case "success":
|
||||
color = greenColor
|
||||
case "failure":
|
||||
color = redColor
|
||||
case "cancelled":
|
||||
color = yellowColor
|
||||
case "skipped":
|
||||
color = purpleColor
|
||||
default:
|
||||
color = greyColor
|
||||
}
|
||||
if withSender {
|
||||
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
// ToHook convert models.Webhook to api.Hook
|
||||
// This function is not part of the convert package to prevent an import cycle
|
||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||
config := map[string]string{
|
||||
"url": w.URL,
|
||||
"content_type": w.ContentType.Name(),
|
||||
}
|
||||
if w.Type == webhook_module.SLACK {
|
||||
s := GetSlackHook(w)
|
||||
config["channel"] = s.Channel
|
||||
config["username"] = s.Username
|
||||
config["icon_url"] = s.IconURL
|
||||
config["color"] = s.Color
|
||||
}
|
||||
|
||||
authorizationHeader, err := w.HeaderAuthorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.Hook{
|
||||
ID: w.ID,
|
||||
Name: w.Name,
|
||||
Type: w.Type,
|
||||
URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
|
||||
Active: w.IsActive,
|
||||
Config: config,
|
||||
Events: w.EventsArray(),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Updated: w.UpdatedUnix.AsTime(),
|
||||
Created: w.CreatedUnix.AsTime(),
|
||||
BranchFilter: w.BranchFilter,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createTestPayload() *api.CreatePayload {
|
||||
return &api.CreatePayload{
|
||||
Sha: "2020558fe2e34debb818a514715839cabd25e777",
|
||||
Ref: "refs/heads/test",
|
||||
RefType: "branch",
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deleteTestPayload() *api.DeletePayload {
|
||||
return &api.DeletePayload{
|
||||
Ref: "refs/heads/test",
|
||||
RefType: "branch",
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func forkTestPayload() *api.ForkPayload {
|
||||
return &api.ForkPayload{
|
||||
Forkee: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo2",
|
||||
Name: "repo2",
|
||||
FullName: "test/repo2",
|
||||
},
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pushTestPayload() *api.PushPayload {
|
||||
return pushTestPayloadWithCommitMessage("commit message")
|
||||
}
|
||||
|
||||
func pushTestMultilineCommitMessagePayload() *api.PushPayload {
|
||||
return pushTestPayloadWithCommitMessage("chore: This is a commit summary\n\nThis is a commit description.")
|
||||
}
|
||||
|
||||
func pushTestPayloadWithCommitMessage(message string) *api.PushPayload {
|
||||
commit := &api.PayloadCommit{
|
||||
ID: "2020558fe2e34debb818a514715839cabd25e778",
|
||||
Message: message,
|
||||
URL: "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
|
||||
Author: &api.PayloadUser{
|
||||
Name: "user1",
|
||||
Email: "user1@localhost",
|
||||
UserName: "user1",
|
||||
},
|
||||
Committer: &api.PayloadUser{
|
||||
Name: "user1",
|
||||
Email: "user1@localhost",
|
||||
UserName: "user1",
|
||||
},
|
||||
}
|
||||
|
||||
return &api.PushPayload{
|
||||
Ref: "refs/heads/test",
|
||||
Before: "2020558fe2e34debb818a514715839cabd25e777",
|
||||
After: "2020558fe2e34debb818a514715839cabd25e778",
|
||||
CompareURL: "",
|
||||
HeadCommit: commit,
|
||||
Commits: []*api.PayloadCommit{commit, commit},
|
||||
TotalCommits: 2,
|
||||
Repo: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Pusher: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func issueTestPayload() *api.IssuePayload {
|
||||
return &api.IssuePayload{
|
||||
Index: 2,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Issue: &api.Issue{
|
||||
ID: 2,
|
||||
Index: 2,
|
||||
URL: "http://localhost:3000/api/v1/repos/test/repo/issues/2",
|
||||
HTMLURL: "http://localhost:3000/test/repo/issues/2",
|
||||
Title: "crash",
|
||||
Body: "issue body",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Assignees: []*api.User{
|
||||
{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
},
|
||||
Milestone: &api.Milestone{
|
||||
ID: 1,
|
||||
Title: "Milestone Title",
|
||||
Description: "Milestone Description",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func issueCommentTestPayload() *api.IssueCommentPayload {
|
||||
return &api.IssueCommentPayload{
|
||||
Action: api.HookIssueCommentCreated,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Comment: &api.Comment{
|
||||
HTMLURL: "http://localhost:3000/test/repo/issues/2#issuecomment-4",
|
||||
IssueURL: "http://localhost:3000/test/repo/issues/2",
|
||||
Body: "more info needed",
|
||||
},
|
||||
Issue: &api.Issue{
|
||||
ID: 2,
|
||||
Index: 2,
|
||||
URL: "http://localhost:3000/api/v1/repos/test/repo/issues/2",
|
||||
HTMLURL: "http://localhost:3000/test/repo/issues/2",
|
||||
Title: "crash",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Body: "this happened",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pullRequestCommentTestPayload() *api.IssueCommentPayload {
|
||||
return &api.IssueCommentPayload{
|
||||
Action: api.HookIssueCommentCreated,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Comment: &api.Comment{
|
||||
HTMLURL: "http://localhost:3000/test/repo/pulls/12#issuecomment-4",
|
||||
PRURL: "http://localhost:3000/test/repo/pulls/12",
|
||||
Body: "changes requested",
|
||||
},
|
||||
Issue: &api.Issue{
|
||||
ID: 12,
|
||||
Index: 12,
|
||||
URL: "http://localhost:3000/api/v1/repos/test/repo/pulls/12",
|
||||
HTMLURL: "http://localhost:3000/test/repo/pulls/12",
|
||||
Title: "Fix bug",
|
||||
Body: "fixes bug #2",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
},
|
||||
IsPull: true,
|
||||
}
|
||||
}
|
||||
|
||||
func wikiTestPayload() *api.WikiPayload {
|
||||
return &api.WikiPayload{
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Page: "index",
|
||||
Comment: "Wiki change comment",
|
||||
}
|
||||
}
|
||||
|
||||
func pullReleaseTestPayload() *api.ReleasePayload {
|
||||
return &api.ReleasePayload{
|
||||
Action: api.HookReleasePublished,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
Release: &api.Release{
|
||||
TagName: "v1.0",
|
||||
Target: "master",
|
||||
Title: "First stable release",
|
||||
Note: "Note of first stable release",
|
||||
HTMLURL: "http://localhost:3000/test/repo/releases/tag/v1.0",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pullRequestTestPayload() *api.PullRequestPayload {
|
||||
return &api.PullRequestPayload{
|
||||
Action: api.HookIssueOpened,
|
||||
Index: 12,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
PullRequest: &api.PullRequest{
|
||||
ID: 12,
|
||||
Index: 12,
|
||||
URL: "http://localhost:3000/test/repo/pulls/12",
|
||||
HTMLURL: "http://localhost:3000/test/repo/pulls/12",
|
||||
Title: "Fix bug",
|
||||
Body: "fixes bug #2",
|
||||
Mergeable: true,
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Assignees: []*api.User{
|
||||
{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
},
|
||||
Milestone: &api.Milestone{
|
||||
ID: 1,
|
||||
Title: "Milestone Title",
|
||||
Description: "Milestone Description",
|
||||
},
|
||||
},
|
||||
Review: &api.ReviewPayload{
|
||||
Content: "good job",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func repositoryTestPayload() *api.RepositoryPayload {
|
||||
return &api.RepositoryPayload{
|
||||
Action: api.HookRepoCreated,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: &api.Repository{
|
||||
HTMLURL: "http://localhost:3000/test/repo",
|
||||
Name: "repo",
|
||||
FullName: "test/repo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func packageTestPayload() *api.PackagePayload {
|
||||
return &api.PackagePayload{
|
||||
Action: api.HookPackageCreated,
|
||||
Sender: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: nil,
|
||||
Organization: &api.Organization{
|
||||
Name: "org1",
|
||||
AvatarURL: "http://localhost:3000/org1/avatar",
|
||||
},
|
||||
Package: &api.Package{
|
||||
Owner: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Repository: nil,
|
||||
Creator: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Type: "container",
|
||||
Name: "GiteaContainer",
|
||||
Version: "latest",
|
||||
HTMLURL: "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssuesPayloadInfo(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
cases := []struct {
|
||||
action api.HookIssueAction
|
||||
text string
|
||||
issueTitle string
|
||||
attachmentText string
|
||||
color int
|
||||
}{
|
||||
{
|
||||
api.HookIssueOpened,
|
||||
"[test/repo] Issue opened: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"issue body",
|
||||
orangeColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueClosed,
|
||||
"[test/repo] Issue closed: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
redColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueReOpened,
|
||||
"[test/repo] Issue re-opened: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueEdited,
|
||||
"[test/repo] Issue edited: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"issue body",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueAssigned,
|
||||
"[test/repo] Issue assigned to user1: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
greenColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueUnassigned,
|
||||
"[test/repo] Issue unassigned: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueLabelUpdated,
|
||||
"[test/repo] Issue labels updated: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueLabelCleared,
|
||||
"[test/repo] Issue labels cleared: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueSynchronized,
|
||||
"[test/repo] Issue synchronized: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueMilestoned,
|
||||
"[test/repo] Issue milestoned to Milestone Title: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueDemilestoned,
|
||||
"[test/repo] Issue milestone cleared: #2 crash by user1",
|
||||
"#2 crash",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p.Action = c.action
|
||||
text, issueTitle, extraMarkdown, color := getIssuesPayloadInfo(p, noneLinkFormatter, true)
|
||||
assert.Equal(t, c.text, text, "case %d", i)
|
||||
assert.Equal(t, c.issueTitle, issueTitle, "case %d", i)
|
||||
assert.Equal(t, c.attachmentText, extraMarkdown, "case %d", i)
|
||||
assert.Equal(t, c.color, color, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPullRequestPayloadInfo(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
cases := []struct {
|
||||
action api.HookIssueAction
|
||||
text string
|
||||
issueTitle string
|
||||
attachmentText string
|
||||
color int
|
||||
}{
|
||||
{
|
||||
api.HookIssueOpened,
|
||||
"[test/repo] Pull request opened: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"fixes bug #2",
|
||||
greenColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueClosed,
|
||||
"[test/repo] Pull request closed: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
redColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueReOpened,
|
||||
"[test/repo] Pull request re-opened: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueEdited,
|
||||
"[test/repo] Pull request edited: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"fixes bug #2",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueAssigned,
|
||||
"[test/repo] Pull request assigned to user1: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
greenColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueUnassigned,
|
||||
"[test/repo] Pull request unassigned: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueLabelUpdated,
|
||||
"[test/repo] Pull request labels updated: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueLabelCleared,
|
||||
"[test/repo] Pull request labels cleared: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueSynchronized,
|
||||
"[test/repo] Pull request synchronized: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueMilestoned,
|
||||
"[test/repo] Pull request milestoned to Milestone Title: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueDemilestoned,
|
||||
"[test/repo] Pull request milestone cleared: #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
"",
|
||||
yellowColor,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p.Action = c.action
|
||||
text, issueTitle, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
|
||||
assert.Equal(t, c.text, text, "case %d", i)
|
||||
assert.Equal(t, c.issueTitle, issueTitle, "case %d", i)
|
||||
assert.Equal(t, c.attachmentText, extraMarkdown, "case %d", i)
|
||||
assert.Equal(t, c.color, color, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWikiPayloadInfo(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
cases := []struct {
|
||||
action api.HookWikiAction
|
||||
text string
|
||||
color int
|
||||
link string
|
||||
}{
|
||||
{
|
||||
api.HookWikiCreated,
|
||||
"[test/repo] New wiki page 'index' (Wiki change comment) by user1",
|
||||
greenColor,
|
||||
"index",
|
||||
},
|
||||
{
|
||||
api.HookWikiEdited,
|
||||
"[test/repo] Wiki page 'index' edited (Wiki change comment) by user1",
|
||||
yellowColor,
|
||||
"index",
|
||||
},
|
||||
{
|
||||
api.HookWikiDeleted,
|
||||
"[test/repo] Wiki page 'index' deleted by user1",
|
||||
redColor,
|
||||
"index",
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p.Action = c.action
|
||||
text, color, link := getWikiPayloadInfo(p, noneLinkFormatter, true)
|
||||
assert.Equal(t, c.text, text, "case %d", i)
|
||||
assert.Equal(t, c.color, color, "case %d", i)
|
||||
assert.Equal(t, c.link, link, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReleasePayloadInfo(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
cases := []struct {
|
||||
action api.HookReleaseAction
|
||||
text string
|
||||
color int
|
||||
}{
|
||||
{
|
||||
api.HookReleasePublished,
|
||||
"[test/repo] Release created: v1.0 by user1",
|
||||
greenColor,
|
||||
},
|
||||
{
|
||||
api.HookReleaseUpdated,
|
||||
"[test/repo] Release updated: v1.0 by user1",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookReleaseDeleted,
|
||||
"[test/repo] Release deleted: v1.0 by user1",
|
||||
redColor,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p.Action = c.action
|
||||
text, color := getReleasePayloadInfo(p, noneLinkFormatter, true)
|
||||
assert.Equal(t, c.text, text, "case %d", i)
|
||||
assert.Equal(t, c.color, color, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssueCommentPayloadInfo(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
cases := []struct {
|
||||
action api.HookIssueCommentAction
|
||||
text string
|
||||
issueTitle string
|
||||
color int
|
||||
}{
|
||||
{
|
||||
api.HookIssueCommentCreated,
|
||||
"[test/repo] New comment on pull request #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
greenColorLight,
|
||||
},
|
||||
{
|
||||
api.HookIssueCommentEdited,
|
||||
"[test/repo] Comment edited on pull request #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
yellowColor,
|
||||
},
|
||||
{
|
||||
api.HookIssueCommentDeleted,
|
||||
"[test/repo] Comment deleted on pull request #12 Fix bug by user1",
|
||||
"#12 Fix bug",
|
||||
redColor,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
p.Action = c.action
|
||||
text, issueTitle, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
|
||||
assert.Equal(t, c.text, text, "case %d", i)
|
||||
assert.Equal(t, c.issueTitle, issueTitle, "case %d", i)
|
||||
assert.Equal(t, c.color, color, "case %d", i)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/hostmatcher"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
_ "gitea.dev/models"
|
||||
_ "gitea.dev/models/actions"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// for tests, allow only loopback IPs
|
||||
setting.Webhook.AllowedHostList = hostmatcher.MatchBuiltinLoopback
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
SetUp: func() error {
|
||||
setting.LoadQueueSettings()
|
||||
return Init()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest)
|
||||
}
|
||||
|
||||
func newMatrixRequest(_ 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)
|
||||
}
|
||||
var pc payloadConvertor[MatrixPayload] = matrixConvertor{
|
||||
MsgType: messageTypeText[meta.MessageType],
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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), w, t, body) // likely useless, but has always been sent historially
|
||||
}
|
||||
|
||||
const matrixPayloadSizeLimit = 1024 * 64
|
||||
|
||||
// MatrixMeta contains the Matrix metadata
|
||||
type MatrixMeta struct {
|
||||
HomeserverURL string `json:"homeserver_url"`
|
||||
Room string `json:"room_id"`
|
||||
MessageType int `json:"message_type"`
|
||||
}
|
||||
|
||||
var messageTypeText = map[int]string{
|
||||
1: "m.notice",
|
||||
2: "m.text",
|
||||
}
|
||||
|
||||
// GetMatrixHook returns Matrix metadata
|
||||
func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
|
||||
s := &MatrixMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||
log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// MatrixPayload contains payload for a Matrix room
|
||||
type MatrixPayload struct {
|
||||
Body string `json:"body"`
|
||||
MsgType string `json:"msgtype"`
|
||||
Format string `json:"format"`
|
||||
FormattedBody string `json:"formatted_body"`
|
||||
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
|
||||
}
|
||||
|
||||
type matrixConvertor struct {
|
||||
MsgType string
|
||||
}
|
||||
|
||||
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 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 m.newPayload(text)
|
||||
}
|
||||
|
||||
// Delete composes Matrix payload for delete a branch or tag.
|
||||
func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) {
|
||||
refName := git.RefName(p.Ref).ShortName()
|
||||
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 m.newPayload(text)
|
||||
}
|
||||
|
||||
// Fork composes Matrix payload for forked by a repository.
|
||||
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 m.newPayload(text)
|
||||
}
|
||||
|
||||
// Issue implements payloadConvertor Issue method
|
||||
func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) {
|
||||
text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// IssueComment implements payloadConvertor IssueComment method
|
||||
func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) {
|
||||
text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// Wiki implements payloadConvertor Wiki method
|
||||
func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) {
|
||||
text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// Release implements payloadConvertor Release method
|
||||
func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) {
|
||||
text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// Push implements payloadConvertor Push method
|
||||
func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
|
||||
var commitDesc string
|
||||
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 commit"
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
|
||||
}
|
||||
|
||||
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
|
||||
branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
|
||||
var text strings.Builder
|
||||
fmt.Fprintf(&text, "[%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 {
|
||||
fmt.Fprintf(&text, "%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.WriteString("<br>")
|
||||
}
|
||||
}
|
||||
|
||||
return m.newPayload(text.String(), p.Commits...)
|
||||
}
|
||||
|
||||
// PullRequest implements payloadConvertor PullRequest method
|
||||
func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) {
|
||||
text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// 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 := 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 MatrixPayload{}, err
|
||||
}
|
||||
|
||||
text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
|
||||
}
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
case api.HookRepoCreated:
|
||||
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
|
||||
}
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
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 {
|
||||
case api.HookPackageCreated:
|
||||
text = fmt.Sprintf("[%s] Package published by %s", packageLink, senderLink)
|
||||
case api.HookPackageDeleted:
|
||||
text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
|
||||
}
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
|
||||
refLink := htmlLinkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
|
||||
text := fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
|
||||
|
||||
func getMessageBody(htmlText string) string {
|
||||
htmlText = urlRegex.ReplaceAllString(htmlText, "[$2]($1)")
|
||||
htmlText = strings.ReplaceAll(htmlText, "<br>", "\n")
|
||||
return htmlText
|
||||
}
|
||||
|
||||
// getMatrixTxnID computes the transaction ID to ensure idempotency
|
||||
func getMatrixTxnID(payload []byte) (string, error) {
|
||||
payload = bytes.TrimSpace(payload)
|
||||
if len(payload) >= matrixPayloadSizeLimit {
|
||||
return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
|
||||
}
|
||||
|
||||
h := sha1.New()
|
||||
_, err := h.Write(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatrixPayload(t *testing.T) {
|
||||
mc := matrixConvertor{
|
||||
MsgType: "m.text",
|
||||
}
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := mc.Create(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] branch created by user1`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := mc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>:test] branch deleted by user1`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := mc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body)
|
||||
assert.Equal(t, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := mc.Push(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := mc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = mc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := mc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := mc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := mc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := mc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := mc.Package(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := mc.Release(p)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pl)
|
||||
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.FormattedBody)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatrixJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.MATRIX,
|
||||
URL: "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message",
|
||||
Meta: `{"message_type":0}`, // text
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newMatrixRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
txnID, ok := strings.CutPrefix(req.URL.Path, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/")
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, txnID, 40) // txnID is just a unique ID for a webhook request, it is a sha1 hash from the payload
|
||||
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
var body MatrixPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body)
|
||||
}
|
||||
|
||||
func Test_getTxnID(t *testing.T) {
|
||||
type args struct {
|
||||
payload []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "dummy payload",
|
||||
args: args{payload: []byte("Hello World")},
|
||||
want: "0a4d55a8d778e5022fab701977c5d840bbc486d0",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getMatrixTxnID(tt.args.payload)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getMatrixTxnID() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// MSTeamsFact for Fact Structure
|
||||
MSTeamsFact struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// MSTeamsSection is a MessageCard section
|
||||
MSTeamsSection struct {
|
||||
ActivityTitle string `json:"activityTitle"`
|
||||
ActivitySubtitle string `json:"activitySubtitle"`
|
||||
ActivityImage string `json:"activityImage"`
|
||||
Facts []MSTeamsFact `json:"facts"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// MSTeamsAction is an action (creates buttons, links etc)
|
||||
MSTeamsAction struct {
|
||||
Type string `json:"@type"`
|
||||
Name string `json:"name"`
|
||||
Targets []MSTeamsActionTarget `json:"targets,omitempty"`
|
||||
}
|
||||
|
||||
// MSTeamsActionTarget is the actual link to follow, etc
|
||||
MSTeamsActionTarget struct {
|
||||
Os string `json:"os"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
// MSTeamsPayload is the parent object
|
||||
MSTeamsPayload struct {
|
||||
Type string `json:"@type"`
|
||||
Context string `json:"@context"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
Sections []MSTeamsSection `json:"sections"`
|
||||
PotentialAction []MSTeamsAction `json:"potentialAction"`
|
||||
}
|
||||
)
|
||||
|
||||
type msteamsConvertor struct{}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
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)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
|
||||
greenColor,
|
||||
&MSTeamsFact{p.RefType + ":", refName},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
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)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
|
||||
yellowColor,
|
||||
&MSTeamsFact{p.RefType + ":", refName},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
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(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Repo.HTMLURL,
|
||||
greenColor,
|
||||
&MSTeamsFact{"Forkee:", p.Forkee.FullName},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
|
||||
var (
|
||||
branchName = git.RefName(p.Ref).ShortName()
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
var titleLink string
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 new commit"
|
||||
titleLink = p.Commits[0].URL
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
||||
titleLink = p.CompareURL
|
||||
}
|
||||
if titleLink == "" {
|
||||
titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
text.String(),
|
||||
titleLink,
|
||||
greenColor,
|
||||
&MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
|
||||
title, _, extraMarkdown, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
extraMarkdown,
|
||||
p.Issue.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
|
||||
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
p.Comment.Body,
|
||||
p.Comment.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// PullRequest implements PayloadConvertor PullRequest method
|
||||
func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
|
||||
title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
extraMarkdown,
|
||||
p.PullRequest.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
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 MSTeamsPayload{}, err
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
||||
text = p.Review.Content
|
||||
|
||||
switch event {
|
||||
case webhook_module.HookEventPullRequestReviewApproved:
|
||||
color = greenColor
|
||||
case webhook_module.HookEventPullRequestReviewRejected:
|
||||
color = redColor
|
||||
case webhook_module.HookEventPullRequestReviewComment:
|
||||
color = greyColor
|
||||
default:
|
||||
color = yellowColor
|
||||
}
|
||||
}
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
text,
|
||||
p.PullRequest.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
|
||||
var title, url string
|
||||
var color int
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
||||
url = p.Repository.HTMLURL
|
||||
color = greenColor
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
color = yellowColor
|
||||
}
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
url,
|
||||
color,
|
||||
nil,
|
||||
), nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
|
||||
title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page),
|
||||
color,
|
||||
&MSTeamsFact{"Repository:", p.Repository.FullName},
|
||||
), nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
|
||||
title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Release.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Tag:", p.Release.TagName},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
|
||||
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Package.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Package:", p.Package.Name},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
|
||||
title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.TargetURL,
|
||||
color,
|
||||
&MSTeamsFact{"CommitStatus:", p.Context},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
|
||||
title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.WorkflowRun.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
|
||||
title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.WorkflowJob.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"WorkflowJob:", p.WorkflowJob.Name},
|
||||
), nil
|
||||
}
|
||||
|
||||
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{
|
||||
Name: "Repository:",
|
||||
Value: r.FullName,
|
||||
})
|
||||
}
|
||||
if fact != nil {
|
||||
facts = append(facts, *fact)
|
||||
}
|
||||
|
||||
return MSTeamsPayload{
|
||||
Type: "MessageCard",
|
||||
Context: "https://schema.org/extensions",
|
||||
ThemeColor: fmt.Sprintf("%x", color),
|
||||
Title: title,
|
||||
Summary: title,
|
||||
Sections: []MSTeamsSection{
|
||||
{
|
||||
ActivityTitle: s.FullName,
|
||||
ActivitySubtitle: s.UserName,
|
||||
ActivityImage: s.AvatarURL,
|
||||
Text: text,
|
||||
Facts: facts,
|
||||
},
|
||||
},
|
||||
PotentialAction: []MSTeamsAction{
|
||||
{
|
||||
Type: "OpenUri",
|
||||
Name: "View in Gitea",
|
||||
Targets: []MSTeamsActionTarget{
|
||||
{
|
||||
Os: "default",
|
||||
URI: actionTarget,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest)
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMSTeamsPayload(t *testing.T) {
|
||||
mc := msteamsConvertor{}
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := mc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] branch test created", pl.Title)
|
||||
assert.Equal(t, "[test/repo] branch test created", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repo.FullName, fact.Value)
|
||||
} else if fact.Name == "branch:" {
|
||||
assert.Equal(t, "test", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := mc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] branch test deleted", pl.Title)
|
||||
assert.Equal(t, "[test/repo] branch test deleted", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repo.FullName, fact.Value)
|
||||
} else if fact.Name == "branch:" {
|
||||
assert.Equal(t, "test", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := mc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title)
|
||||
assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repo.FullName, fact.Value)
|
||||
} else if fact.Name == "Forkee:" {
|
||||
assert.Equal(t, p.Forkee.FullName, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := mc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repo.FullName, fact.Value)
|
||||
} else if fact.Name == "Commit count:" {
|
||||
assert.Equal(t, "2", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := mc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "issue body", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Issue #:" {
|
||||
assert.Equal(t, "2", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = mc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Issue #:" {
|
||||
assert.Equal(t, "2", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := mc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title)
|
||||
assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "more info needed", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Issue #:" {
|
||||
assert.Equal(t, "2", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := mc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "fixes bug #2", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Pull request #:" {
|
||||
assert.Equal(t, "12", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := mc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title)
|
||||
assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "changes requested", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Issue #:" {
|
||||
assert.Equal(t, "12", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Equal(t, "good job", pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Pull request #:" {
|
||||
assert.Equal(t, "12", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := mc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Repository created", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 1)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := mc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title)
|
||||
assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 1)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Package:" {
|
||||
assert.Equal(t, p.Package.Name, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title)
|
||||
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = mc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := mc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title)
|
||||
assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary)
|
||||
assert.Len(t, pl.Sections, 1)
|
||||
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
|
||||
assert.Empty(t, pl.Sections[0].Text)
|
||||
assert.Len(t, pl.Sections[0].Facts, 2)
|
||||
for _, fact := range pl.Sections[0].Facts {
|
||||
if fact.Name == "Repository:" {
|
||||
assert.Equal(t, p.Repository.FullName, fact.Value)
|
||||
} else if fact.Name == "Tag:" {
|
||||
assert.Equal(t, "v1.0", fact.Value)
|
||||
} else {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
assert.Len(t, pl.PotentialAction, 1)
|
||||
assert.Len(t, pl.PotentialAction[0].Targets, 1)
|
||||
assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMSTeamsJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.MSTEAMS,
|
||||
URL: "https://msteams.example.com/",
|
||||
Meta: ``,
|
||||
HTTPMethod: "POST",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newMSTeamsRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://msteams.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 MSTeamsPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// PackagistPayload represents
|
||||
PackagistPayload struct {
|
||||
PackagistRepository struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
// PackagistMeta contains the metadata for the webhook
|
||||
PackagistMeta struct {
|
||||
Username string `json:"username"`
|
||||
APIToken string `json:"api_token"`
|
||||
PackageURL string `json:"package_url"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetPackagistHook returns packagist metadata
|
||||
func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
|
||||
s := &PackagistMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||
log.Error("webhook.GetPackagistHook(%d): %v", w.ID, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type packagistConvertor struct {
|
||||
PackageURL string
|
||||
}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
// 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 (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// PullRequest implements PayloadConvertor PullRequest method
|
||||
func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
|
||||
return PackagistPayload{}, nil
|
||||
}
|
||||
|
||||
func newPackagistRequest(_ 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)
|
||||
}
|
||||
var pc payloadConvertor[PackagistPayload] = packagistConvertor{
|
||||
PackageURL: meta.PackageURL,
|
||||
}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackagistPayload(t *testing.T) {
|
||||
pc := packagistConvertor{
|
||||
PackageURL: "https://packagist.org/packages/example",
|
||||
}
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := pc.Create(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := pc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := pc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := pc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := pc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = pc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := pc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := pc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := pc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := pc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := pc.Package(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := pc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = pc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = pc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := pc.Release(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PackagistPayload{}, pl)
|
||||
})
|
||||
}
|
||||
|
||||
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(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
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(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.Empty(t, body.PackagistRepository.URL)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
// 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)
|
||||
Status(*api.CommitStatusPayload) (T, error)
|
||||
WorkflowRun(*api.WorkflowRunPayload) (T, error)
|
||||
WorkflowJob(*api.WorkflowJobPayload) (T, error)
|
||||
}
|
||||
|
||||
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
|
||||
var p P
|
||||
if err = json.Unmarshal(data, &p); err != nil {
|
||||
return t, fmt.Errorf("could not unmarshal payload: %w", err)
|
||||
}
|
||||
return convert(p)
|
||||
}
|
||||
|
||||
func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (t T, err error) {
|
||||
switch event {
|
||||
case webhook_module.HookEventCreate:
|
||||
return convertUnmarshalledJSON(rc.Create, data)
|
||||
case webhook_module.HookEventDelete:
|
||||
return convertUnmarshalledJSON(rc.Delete, data)
|
||||
case webhook_module.HookEventFork:
|
||||
return convertUnmarshalledJSON(rc.Fork, data)
|
||||
case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
|
||||
return convertUnmarshalledJSON(rc.Issue, data)
|
||||
case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
|
||||
// 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 convertUnmarshalledJSON(rc.Push, data)
|
||||
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
|
||||
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
|
||||
return convertUnmarshalledJSON(rc.PullRequest, data)
|
||||
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
|
||||
return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
|
||||
return rc.Review(p, event)
|
||||
}, data)
|
||||
case webhook_module.HookEventRepository:
|
||||
return convertUnmarshalledJSON(rc.Repository, data)
|
||||
case webhook_module.HookEventRelease:
|
||||
return convertUnmarshalledJSON(rc.Release, data)
|
||||
case webhook_module.HookEventWiki:
|
||||
return convertUnmarshalledJSON(rc.Wiki, data)
|
||||
case webhook_module.HookEventPackage:
|
||||
return convertUnmarshalledJSON(rc.Package, data)
|
||||
case webhook_module.HookEventStatus:
|
||||
return convertUnmarshalledJSON(rc.Status, data)
|
||||
case webhook_module.HookEventWorkflowRun:
|
||||
return convertUnmarshalledJSON(rc.WorkflowRun, data)
|
||||
case webhook_module.HookEventWorkflowJob:
|
||||
return convertUnmarshalledJSON(rc.WorkflowJob, data)
|
||||
}
|
||||
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
|
||||
}
|
||||
return prepareJSONRequest(payload, w, t, withDefaultHeaders)
|
||||
}
|
||||
|
||||
func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
|
||||
body, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
method := w.HTTPMethod
|
||||
if method == "" {
|
||||
method = http.MethodPost
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, 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), w, t, body)
|
||||
}
|
||||
return req, body, nil
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
// SlackMeta contains the slack metadata
|
||||
type SlackMeta struct {
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// GetSlackHook returns slack metadata
|
||||
func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
|
||||
s := &SlackMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||
log.Error("webhook.GetSlackHook(%d): %v", w.ID, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SlackPayload contains the information about the slack channel
|
||||
type SlackPayload struct {
|
||||
Channel string `json:"channel"`
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
UnfurlLinks int `json:"unfurl_links"`
|
||||
LinkNames int `json:"link_names"`
|
||||
Attachments []SlackAttachment `json:"attachments"`
|
||||
}
|
||||
|
||||
// SlackAttachment contains the slack message
|
||||
type SlackAttachment struct {
|
||||
Fallback string `json:"fallback"`
|
||||
Color string `json:"color"`
|
||||
Title string `json:"title"`
|
||||
TitleLink string `json:"title_link"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// SlackTextFormatter replaces &, <, > with HTML characters
|
||||
// see: https://api.slack.com/docs/formatting
|
||||
func SlackTextFormatter(s string) string {
|
||||
// replace & < >
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
return s
|
||||
}
|
||||
|
||||
// SlackShortTextFormatter replaces &, <, > with HTML characters
|
||||
func SlackShortTextFormatter(s string) string {
|
||||
s = strings.Split(s, "\n")[0]
|
||||
// replace & < >
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
return s
|
||||
}
|
||||
|
||||
// SlackLinkFormatter creates a link compatible with slack
|
||||
func SlackLinkFormatter(url, text string) string {
|
||||
return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
|
||||
}
|
||||
|
||||
// SlackLinkToRef slack-formatter link to a repo ref
|
||||
func SlackLinkToRef(repoURL, ref string) string {
|
||||
// FIXME: SHA1 hardcoded here
|
||||
refName := git.RefName(ref)
|
||||
url := repoURL + "/src/" + refName.RefWebLinkPath()
|
||||
return SlackLinkFormatter(url, refName.ShortName())
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// Delete composes Slack payload for delete a branch or tag.
|
||||
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)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// Fork composes Slack payload for forked by a repository.
|
||||
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 slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) {
|
||||
text, issueTitle, extraMarkdown, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
var attachments []SlackAttachment
|
||||
if extraMarkdown != "" {
|
||||
extraMarkdown = SlackTextFormatter(extraMarkdown)
|
||||
issueTitle = SlackTextFormatter(issueTitle)
|
||||
attachments = append(attachments, SlackAttachment{
|
||||
Color: fmt.Sprintf("%x", color),
|
||||
Title: issueTitle,
|
||||
TitleLink: p.Issue.HTMLURL,
|
||||
Text: extraMarkdown,
|
||||
})
|
||||
}
|
||||
|
||||
return s.createPayload(text, attachments), nil
|
||||
}
|
||||
|
||||
// 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{{
|
||||
Color: fmt.Sprintf("%x", color),
|
||||
Title: issueTitle,
|
||||
TitleLink: p.Comment.HTMLURL,
|
||||
Text: SlackTextFormatter(p.Comment.Body),
|
||||
}}), nil
|
||||
}
|
||||
|
||||
// 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 slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) {
|
||||
text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
|
||||
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) {
|
||||
text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// Push implements payloadConvertor Push method
|
||||
func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
|
||||
// n new commits
|
||||
var (
|
||||
commitDesc string
|
||||
commitString string
|
||||
)
|
||||
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 new commit"
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
||||
}
|
||||
if len(p.CompareURL) > 0 {
|
||||
commitString = SlackLinkFormatter(p.CompareURL, commitDesc)
|
||||
} else {
|
||||
commitString = commitDesc
|
||||
}
|
||||
|
||||
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
|
||||
branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
|
||||
text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
|
||||
|
||||
var attachmentText strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
fmt.Fprintf(&attachmentText, "%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
attachmentText.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return s.createPayload(text, []SlackAttachment{{
|
||||
Color: s.Color,
|
||||
Title: p.Repo.HTMLURL,
|
||||
TitleLink: p.Repo.HTMLURL,
|
||||
Text: attachmentText.String(),
|
||||
}}), nil
|
||||
}
|
||||
|
||||
// PullRequest implements payloadConvertor PullRequest method
|
||||
func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) {
|
||||
text, issueTitle, extraMarkdown, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
var attachments []SlackAttachment
|
||||
if extraMarkdown != "" {
|
||||
extraMarkdown = SlackTextFormatter(p.PullRequest.Body)
|
||||
issueTitle = SlackTextFormatter(issueTitle)
|
||||
attachments = append(attachments, SlackAttachment{
|
||||
Color: fmt.Sprintf("%x", color),
|
||||
Title: issueTitle,
|
||||
TitleLink: p.PullRequest.HTMLURL,
|
||||
Text: extraMarkdown,
|
||||
})
|
||||
}
|
||||
|
||||
return s.createPayload(text, attachments), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
var text string
|
||||
|
||||
switch p.Action {
|
||||
case api.HookIssueReviewed:
|
||||
action, err := parseHookPullRequestEventType(event)
|
||||
if err != nil {
|
||||
return SlackPayload{}, err
|
||||
}
|
||||
|
||||
text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
|
||||
}
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
|
||||
}
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
|
||||
return SlackPayload{
|
||||
Channel: s.Channel,
|
||||
Text: text,
|
||||
Username: s.Username,
|
||||
IconURL: s.IconURL,
|
||||
Attachments: attachments,
|
||||
}
|
||||
}
|
||||
|
||||
type slackConvertor struct {
|
||||
Channel string
|
||||
Username string
|
||||
IconURL string
|
||||
Color string
|
||||
}
|
||||
|
||||
func newSlackRequest(_ 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)
|
||||
}
|
||||
var pc payloadConvertor[SlackPayload] = slackConvertor{
|
||||
Channel: meta.Channel,
|
||||
Username: meta.Username,
|
||||
IconURL: meta.IconURL,
|
||||
Color: meta.Color,
|
||||
}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest)
|
||||
}
|
||||
|
||||
var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
|
||||
|
||||
// IsValidSlackChannel validates a channel name conforms to what slack expects:
|
||||
// https://api.slack.com/methods/conversations.rename#naming
|
||||
// Conversation names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less.
|
||||
// Gitea accepts if it starts with a #.
|
||||
func IsValidSlackChannel(name string) bool {
|
||||
return slackChannel.MatchString(name)
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSlackPayload(t *testing.T) {
|
||||
sc := slackConvertor{}
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := sc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := sc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := sc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := sc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := sc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = sc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := sc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := sc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := sc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := sc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := sc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := sc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = sc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = sc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := sc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlackJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.SLACK,
|
||||
URL: "https://slack.example.com/",
|
||||
Meta: `{}`,
|
||||
HTTPMethod: "POST",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newSlackRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://slack.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 SlackPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", body.Text)
|
||||
}
|
||||
|
||||
func TestIsValidSlackChannel(t *testing.T) {
|
||||
tt := []struct {
|
||||
channelName string
|
||||
expected bool
|
||||
}{
|
||||
{"gitea", true},
|
||||
{"#gitea", true},
|
||||
{" ", false},
|
||||
{"#", false},
|
||||
{" #", false},
|
||||
{"gitea ", false},
|
||||
{" gitea", false},
|
||||
}
|
||||
|
||||
for _, v := range tt {
|
||||
assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// TelegramPayload represents
|
||||
TelegramPayload struct {
|
||||
Message string `json:"text"`
|
||||
ParseMode string `json:"parse_mode"`
|
||||
DisableWebPreview bool `json:"disable_web_page_preview"`
|
||||
}
|
||||
|
||||
// TelegramMeta contains the telegram metadata
|
||||
TelegramMeta struct {
|
||||
BotToken string `json:"bot_token"`
|
||||
ChatID string `json:"chat_id"`
|
||||
ThreadID string `json:"thread_id"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetTelegramHook returns telegram metadata
|
||||
func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
|
||||
s := &TelegramMeta{}
|
||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||
log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type telegramConvertor struct{}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) {
|
||||
// created tag/branch
|
||||
refName := git.RefName(p.Ref).ShortName()
|
||||
title := fmt.Sprintf(`[%s] %s %s created`,
|
||||
htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName),
|
||||
html.EscapeString(p.RefType),
|
||||
htmlLinkFormatter(p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), refName),
|
||||
)
|
||||
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) {
|
||||
// created tag/branch
|
||||
refName := git.RefName(p.Ref).ShortName()
|
||||
title := fmt.Sprintf(`[%s] %s %s deleted`,
|
||||
htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName),
|
||||
html.EscapeString(p.RefType),
|
||||
htmlLinkFormatter(p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), refName),
|
||||
)
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) {
|
||||
title := fmt.Sprintf(`%s is forked to %s`, html.EscapeString(p.Forkee.FullName), htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName))
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
|
||||
branchName := git.RefName(p.Ref).ShortName()
|
||||
var titleLink, commitDesc string
|
||||
if p.TotalCommits == 1 {
|
||||
commitDesc = "1 new commit"
|
||||
titleLink = p.Commits[0].URL
|
||||
} else {
|
||||
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
||||
titleLink = p.CompareURL
|
||||
}
|
||||
if titleLink == "" {
|
||||
titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
|
||||
}
|
||||
title := fmt.Sprintf(`[%s:%s] %s`, htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName), htmlLinkFormatter(titleLink, branchName), html.EscapeString(commitDesc))
|
||||
|
||||
var htmlCommits strings.Builder
|
||||
for _, commit := range p.Commits {
|
||||
fmt.Fprintf(&htmlCommits, "\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n")))
|
||||
if commit.Author != nil {
|
||||
htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name))
|
||||
}
|
||||
}
|
||||
return createTelegramPayloadHTML(title + htmlCommits.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) {
|
||||
text, _, extraMarkdown, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
|
||||
// TODO: at the moment the markdown can't be rendered easily because it has context-aware links (eg: attachments)
|
||||
return createTelegramPayloadHTML(text + "\n\n" + html.EscapeString(extraMarkdown)), nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) {
|
||||
text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
|
||||
return createTelegramPayloadHTML(text + "\n" + html.EscapeString(p.Comment.Body)), nil
|
||||
}
|
||||
|
||||
// PullRequest implements PayloadConvertor PullRequest method
|
||||
func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) {
|
||||
text, _, extraMarkdown, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
|
||||
return createTelegramPayloadHTML(text + "\n" + html.EscapeString(extraMarkdown)), nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) {
|
||||
var text, extraMarkdown string
|
||||
switch p.Action {
|
||||
case api.HookIssueReviewed:
|
||||
action, err := parseHookPullRequestEventType(event)
|
||||
if err != nil {
|
||||
return TelegramPayload{}, err
|
||||
}
|
||||
|
||||
text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", html.EscapeString(p.Repository.FullName), html.EscapeString(action), p.Index, html.EscapeString(p.PullRequest.Title))
|
||||
extraMarkdown = p.Review.Content
|
||||
}
|
||||
|
||||
return createTelegramPayloadHTML(text + "\n" + html.EscapeString(extraMarkdown)), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) {
|
||||
var title string
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
title = fmt.Sprintf(`[%s] Repository created`, htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName))
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", html.EscapeString(p.Repository.FullName))
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
}
|
||||
return TelegramPayload{}, nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) {
|
||||
text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) {
|
||||
text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) {
|
||||
text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) {
|
||||
text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayloadHTML(text), nil
|
||||
}
|
||||
|
||||
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
|
||||
// https://core.telegram.org/bots/api#formatting-options
|
||||
return TelegramPayload{
|
||||
Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))),
|
||||
ParseMode: "HTML",
|
||||
DisableWebPreview: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[TelegramPayload] = telegramConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest)
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTelegramPayload(t *testing.T) {
|
||||
tc := telegramConvertor{}
|
||||
|
||||
t.Run("Correct webhook params", func(t *testing.T) {
|
||||
p := createTelegramPayloadHTML(`<a href=".">testMsg</a> <bad>`)
|
||||
assert.Equal(t, TelegramPayload{
|
||||
Message: `<a href="." rel="nofollow">testMsg</a>`,
|
||||
ParseMode: "HTML",
|
||||
DisableWebPreview: true,
|
||||
}, p)
|
||||
})
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
p := createTestPayload()
|
||||
|
||||
pl, err := tc.Create(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a> created`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
p := deleteTestPayload()
|
||||
|
||||
pl, err := tc.Delete(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a> deleted`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Fork", func(t *testing.T) {
|
||||
p := forkTestPayload()
|
||||
|
||||
pl, err := tc.Fork(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Push", func(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
|
||||
pl, err := tc.Push(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits
|
||||
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1
|
||||
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Issue", func(t *testing.T) {
|
||||
p := issueTestPayload()
|
||||
|
||||
p.Action = api.HookIssueOpened
|
||||
pl, err := tc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>
|
||||
|
||||
issue body`, pl.Message)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = tc.Issue(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
p := issueCommentTestPayload()
|
||||
|
||||
pl, err := tc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2" rel="nofollow">#2 crash</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>
|
||||
more info needed`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
|
||||
pl, err := tc.PullRequest(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>
|
||||
fixes bug #2`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
p := pullRequestCommentTestPayload()
|
||||
|
||||
pl, err := tc.IssueComment(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12" rel="nofollow">#12 Fix bug</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>
|
||||
changes requested`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
p := pullRequestTestPayload()
|
||||
p.Action = api.HookIssueReviewed
|
||||
|
||||
pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug
|
||||
good job`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
p := repositoryTestPayload()
|
||||
|
||||
pl, err := tc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Repository created`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Package", func(t *testing.T) {
|
||||
p := packageTestPayload()
|
||||
|
||||
pl, err := tc.Package(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest" rel="nofollow">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
p := wikiTestPayload()
|
||||
|
||||
p.Action = api.HookWikiCreated
|
||||
pl, err := tc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index" rel="nofollow">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
|
||||
p.Action = api.HookWikiEdited
|
||||
pl, err = tc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index" rel="nofollow">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
|
||||
p.Action = api.HookWikiDeleted
|
||||
pl, err = tc.Wiki(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index" rel="nofollow">index</a>' deleted by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
})
|
||||
|
||||
t.Run("Release", func(t *testing.T) {
|
||||
p := pullReleaseTestPayload()
|
||||
|
||||
pl, err := tc.Release(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0" rel="nofollow">v1.0</a> by <a href="https://try.gitea.io/user1" rel="nofollow">user1</a>`, pl.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTelegramJSONPayload(t *testing.T) {
|
||||
p := pushTestPayload()
|
||||
data, err := p.JSONPayload()
|
||||
require.NoError(t, err)
|
||||
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: 3,
|
||||
IsActive: true,
|
||||
Type: webhook_module.TELEGRAM,
|
||||
URL: "https://telegram.example.com/",
|
||||
Meta: ``,
|
||||
HTTPMethod: "POST",
|
||||
}
|
||||
task := &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadContent: string(data),
|
||||
PayloadVersion: 2,
|
||||
}
|
||||
|
||||
req, reqBody, err := newTelegramRequest(t.Context(), hook, task)
|
||||
require.NotNil(t, req)
|
||||
require.NotNil(t, reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://telegram.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 TelegramPayload
|
||||
err = json.NewDecoder(req.Body).Decode(&body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `[<a href="http://localhost:3000/test/repo" rel="nofollow">test/repo</a>:<a href="http://localhost:3000/test/repo/src/test" rel="nofollow">test</a>] 2 new commits
|
||||
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1
|
||||
[<a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778" rel="nofollow">2020558</a>] commit message - user1`, body.Message)
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
|
||||
|
||||
var webhookRequesters = map[webhook_module.HookType]Requester{}
|
||||
|
||||
func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) {
|
||||
webhookRequesters[hookType] = requester
|
||||
}
|
||||
|
||||
// IsValidHookTaskType returns true if a webhook registered
|
||||
func IsValidHookTaskType(name string) bool {
|
||||
if name == webhook_module.GITEA || name == webhook_module.GOGS {
|
||||
return true
|
||||
}
|
||||
_, ok := webhookRequesters[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// hookQueue is a global queue of web hooks
|
||||
var hookQueue *queue.WorkerPoolQueue[int64]
|
||||
|
||||
// getPayloadRef returns the full ref name for hook event, if applicable.
|
||||
func getPayloadRef(p api.Payloader) git.RefName {
|
||||
switch pp := p.(type) {
|
||||
case *api.CreatePayload:
|
||||
switch pp.RefType {
|
||||
case "branch":
|
||||
return git.RefNameFromBranch(pp.Ref)
|
||||
case "tag":
|
||||
return git.RefNameFromTag(pp.Ref)
|
||||
}
|
||||
case *api.DeletePayload:
|
||||
switch pp.RefType {
|
||||
case "branch":
|
||||
return git.RefNameFromBranch(pp.Ref)
|
||||
case "tag":
|
||||
return git.RefNameFromTag(pp.Ref)
|
||||
}
|
||||
case *api.PushPayload:
|
||||
return git.RefName(pp.Ref)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// EventSource represents the source of a webhook action. Repository and/or Owner must be set.
|
||||
type EventSource struct {
|
||||
Repository *repo_model.Repository
|
||||
Owner *user_model.User
|
||||
}
|
||||
|
||||
// handle delivers hook tasks
|
||||
func handler(items ...int64) []int64 {
|
||||
ctx := graceful.GetManager().HammerContext()
|
||||
|
||||
for _, taskID := range items {
|
||||
task, err := webhook_model.GetHookTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err)
|
||||
} else {
|
||||
log.Error("GetHookTaskByID[%d] failed: %v", taskID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if task.IsDelivered {
|
||||
// Already delivered in the meantime
|
||||
log.Trace("Task[%d] has already been delivered", task.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := Deliver(ctx, task); err != nil {
|
||||
log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enqueueHookTask(taskID int64) error {
|
||||
err := hookQueue.Push(taskID)
|
||||
if err != nil && err != queue.ErrAlreadyInQueue {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkBranchFilter(branchFilter string, ref git.RefName) bool {
|
||||
if branchFilter == "" || branchFilter == "*" || branchFilter == "**" {
|
||||
return true
|
||||
}
|
||||
|
||||
g, err := glob.Compile(branchFilter)
|
||||
if err != nil {
|
||||
// should not really happen as BranchFilter is validated
|
||||
log.Debug("checkBranchFilter failed to compile filer %q, err: %s", branchFilter, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if ref.IsBranch() && g.Match(ref.BranchName()) {
|
||||
return true
|
||||
}
|
||||
return g.Match(ref.String())
|
||||
}
|
||||
|
||||
// PrepareWebhook creates a hook task and enqueues it for processing.
|
||||
// The payload is saved as-is. The adjustments depending on the webhook type happen
|
||||
// right before delivery, in the [Deliver] method.
|
||||
func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
|
||||
// Skip sending if webhooks are disabled.
|
||||
if setting.DisableWebhooks {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !w.HasEvent(event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
|
||||
// Integration webhooks (e.g. drone) still receive the required data.
|
||||
if pushEvent, ok := p.(*api.PushPayload); ok &&
|
||||
w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
|
||||
len(pushEvent.Commits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If payload has no associated branch (e.g. it's a new tag, issue, etc.), branch filter has no effect.
|
||||
if ref := getPayloadRef(p); ref != "" {
|
||||
// Check the payload's git ref against the webhook's branch filter.
|
||||
if !checkBranchFilter(w.BranchFilter, ref) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := p.JSONPayload()
|
||||
if err != nil {
|
||||
return fmt.Errorf("JSONPayload for %s: %w", event, err)
|
||||
}
|
||||
|
||||
task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
|
||||
HookID: w.ID,
|
||||
PayloadContent: string(payload),
|
||||
EventType: event,
|
||||
PayloadVersion: 2,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateHookTask for %s: %w", event, err)
|
||||
}
|
||||
|
||||
return enqueueHookTask(task.ID)
|
||||
}
|
||||
|
||||
// PrepareWebhooks adds new webhooks to task queue for given payload.
|
||||
func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error {
|
||||
owner := source.Owner
|
||||
|
||||
var ws []*webhook_model.Webhook
|
||||
|
||||
if source.Repository != nil {
|
||||
repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
|
||||
RepoID: source.Repository.ID,
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ListWebhooksByOpts: %w", err)
|
||||
}
|
||||
ws = append(ws, repoHooks...)
|
||||
|
||||
owner = source.Repository.MustOwner(ctx)
|
||||
}
|
||||
|
||||
// append additional webhooks of a user or organization
|
||||
if owner != nil {
|
||||
ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
|
||||
OwnerID: owner.ID,
|
||||
IsActive: optional.Some(true),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ListWebhooksByOpts: %w", err)
|
||||
}
|
||||
ws = append(ws, ownerHooks...)
|
||||
}
|
||||
|
||||
// Add any admin-defined system webhooks
|
||||
systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetSystemWebhooks: %w", err)
|
||||
}
|
||||
ws = append(ws, systemHooks...)
|
||||
|
||||
if len(ws) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, w := range ws {
|
||||
if err := PrepareWebhook(ctx, w, event, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplayHookTask replays a webhook task
|
||||
func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error {
|
||||
task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return enqueueHookTask(task.ID)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebhookService(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
t.Run("GetSlackHook", testWebhookGetSlackHook)
|
||||
t.Run("PrepareWebhooks", testWebhookPrepare)
|
||||
t.Run("PrepareBranchFilterMatch", testWebhookPrepareBranchFilterMatch)
|
||||
t.Run("PrepareBranchFilterNoMatch", testWebhookPrepareBranchFilterNoMatch)
|
||||
t.Run("WebhookUserMail", testWebhookUserMail)
|
||||
t.Run("CheckBranchFilter", testWebhookCheckBranchFilter)
|
||||
}
|
||||
|
||||
func testWebhookGetSlackHook(t *testing.T) {
|
||||
w := &webhook_model.Webhook{
|
||||
Meta: `{"channel": "foo", "username": "username", "color": "blue"}`,
|
||||
}
|
||||
slackHook := GetSlackHook(w)
|
||||
assert.Equal(t, SlackMeta{
|
||||
Channel: "foo",
|
||||
Username: "username",
|
||||
Color: "blue",
|
||||
}, *slackHook)
|
||||
}
|
||||
|
||||
func testWebhookPrepare(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: repo.ID,
|
||||
URL: "http://localhost/gitea-webhook-test-prepare_webhooks",
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Events: `{"push_only":true}`,
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), hook))
|
||||
|
||||
hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush}
|
||||
unittest.AssertNotExistsBean(t, hookTask)
|
||||
err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, hookTask)
|
||||
}
|
||||
|
||||
func testWebhookPrepareBranchFilterMatch(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: repo.ID,
|
||||
URL: "http://localhost/gitea-webhook-test-branch_filter_match",
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), hook))
|
||||
|
||||
hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush}
|
||||
unittest.AssertNotExistsBean(t, hookTask)
|
||||
// this test also ensures that * doesn't handle / in any special way (like shell would)
|
||||
err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, hookTask)
|
||||
}
|
||||
|
||||
func testWebhookPrepareBranchFilterNoMatch(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
hook := &webhook_model.Webhook{
|
||||
RepoID: repo.ID,
|
||||
URL: "http://localhost/gitea-webhook-test-branch_filter_no_match",
|
||||
ContentType: webhook_model.ContentTypeJSON,
|
||||
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), hook))
|
||||
|
||||
hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush}
|
||||
unittest.AssertNotExistsBean(t, hookTask)
|
||||
err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})
|
||||
require.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, hookTask)
|
||||
}
|
||||
|
||||
func testWebhookUserMail(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(t.Context(), user, nil).Email)
|
||||
assert.Equal(t, user.Email, convert.ToUser(t.Context(), user, user).Email)
|
||||
}
|
||||
|
||||
func testWebhookCheckBranchFilter(t *testing.T) {
|
||||
cases := []struct {
|
||||
filter string
|
||||
ref git.RefName
|
||||
match bool
|
||||
}{
|
||||
{"", "any-ref", true},
|
||||
{"*", "any-ref", true},
|
||||
{"**", "any-ref", true},
|
||||
|
||||
{"main", git.RefNameFromBranch("main"), true},
|
||||
{"main", git.RefNameFromTag("main"), false},
|
||||
|
||||
{"feature/*", git.RefNameFromBranch("feature"), false},
|
||||
{"feature/*", git.RefNameFromBranch("feature/foo"), true},
|
||||
{"feature/*", git.RefNameFromTag("feature/foo"), false},
|
||||
|
||||
{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromBranch("feature/foo"), true},
|
||||
{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromBranch("main"), false},
|
||||
{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromTag("release/bar"), true},
|
||||
{"{refs/heads/feature/*,refs/tags/release/*}", git.RefNameFromTag("dev"), false},
|
||||
}
|
||||
for _, v := range cases {
|
||||
assert.Equal(t, v.match, checkBranchFilter(v.filter, v.ref), "filter: %q ref: %q", v.filter, v.ref)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
)
|
||||
|
||||
type (
|
||||
// WechatworkPayload represents
|
||||
WechatworkPayload struct {
|
||||
Msgtype string `json:"msgtype"`
|
||||
Text struct {
|
||||
Content string `json:"content"`
|
||||
MentionedList []string `json:"mentioned_list"`
|
||||
MentionedMobileList []string `json:"mentioned_mobile_list"`
|
||||
} `json:"text"`
|
||||
Markdown struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"markdown"`
|
||||
}
|
||||
)
|
||||
|
||||
func newWechatworkMarkdownPayload(title string) WechatworkPayload {
|
||||
return WechatworkPayload{
|
||||
Msgtype: "markdown",
|
||||
Markdown: struct {
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
Content: title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type wechatworkConvertor struct{}
|
||||
|
||||
// Create implements PayloadConvertor Create method
|
||||
func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) {
|
||||
// created tag/branch
|
||||
refName := git.RefName(p.Ref).ShortName()
|
||||
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
|
||||
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
}
|
||||
|
||||
// Delete implements PayloadConvertor Delete method
|
||||
func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) {
|
||||
// created tag/branch
|
||||
refName := git.RefName(p.Ref).ShortName()
|
||||
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
|
||||
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
}
|
||||
|
||||
// Fork implements PayloadConvertor Fork method
|
||||
func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) {
|
||||
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
|
||||
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) {
|
||||
var (
|
||||
branchName = git.RefName(p.Ref).ShortName()
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
title := fmt.Sprintf("# %s:%s <font color=\"warning\"> %s </font>", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
if commit.Author != nil {
|
||||
authorName = "Author: " + commit.Author.Name
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n")
|
||||
fmt.Fprintf(&text, " > [%s](%s) \r\n ><font color=\"info\">%s</font> \n ><font color=\"warning\">%s</font>", commit.ID[:7], commit.URL,
|
||||
message, authorName)
|
||||
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return newWechatworkMarkdownPayload(title + "\r\n\r\n" + text.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) {
|
||||
text, issueTitle, extraMarkdown, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
|
||||
var content string
|
||||
content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\"> %s</font> \n [%s](%s)", text, extraMarkdown, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL)
|
||||
|
||||
return newWechatworkMarkdownPayload(content), nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) {
|
||||
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
|
||||
var content string
|
||||
content += fmt.Sprintf(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\">%s</font> \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL)
|
||||
|
||||
return newWechatworkMarkdownPayload(content), nil
|
||||
}
|
||||
|
||||
// PullRequest implements PayloadConvertor PullRequest method
|
||||
func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) {
|
||||
text, issueTitle, extraMarkdown, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
|
||||
pr := fmt.Sprintf("> <font color=\"info\"> %s </font> \r\n > <font color=\"comment\">%s </font> \r\n > <font color=\"comment\">%s </font> \r\n",
|
||||
text, issueTitle, extraMarkdown)
|
||||
|
||||
return newWechatworkMarkdownPayload(pr), nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) {
|
||||
var text, title string
|
||||
switch p.Action {
|
||||
case api.HookIssueReviewed:
|
||||
action, err := parseHookPullRequestEventType(event)
|
||||
if err != nil {
|
||||
return WechatworkPayload{}, err
|
||||
}
|
||||
title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
||||
text = p.Review.Content
|
||||
}
|
||||
|
||||
return newWechatworkMarkdownPayload("# " + title + "\r\n\r\n >" + text), nil
|
||||
}
|
||||
|
||||
// Repository implements PayloadConvertor Repository method
|
||||
func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) {
|
||||
var title string
|
||||
switch p.Action {
|
||||
case api.HookRepoCreated:
|
||||
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
}
|
||||
|
||||
return WechatworkPayload{}, nil
|
||||
}
|
||||
|
||||
// Wiki implements PayloadConvertor Wiki method
|
||||
func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) {
|
||||
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
// Release implements PayloadConvertor Release method
|
||||
func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) {
|
||||
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) {
|
||||
text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) {
|
||||
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
|
||||
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||
var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
||||
return newJSONRequest(pc, w, t, true)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest)
|
||||
}
|
||||
Reference in New Issue
Block a user