mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
[REFACTOR] webhook.Handler interface
This commit is contained in:
parent
142459bbe0
commit
702152bfde
35 changed files with 378 additions and 210 deletions
|
@ -342,4 +342,5 @@ package "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
package "code.gitea.io/gitea/services/webhook"
|
package "code.gitea.io/gitea/services/webhook"
|
||||||
func NewNotifier
|
func NewNotifier
|
||||||
|
func List
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
url: http://www.example.com/url1
|
url: http://www.example.com/url1
|
||||||
http_method: POST
|
http_method: POST
|
||||||
|
type: forgejo
|
||||||
content_type: 1 # json
|
content_type: 1 # json
|
||||||
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
|
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
|
||||||
is_active: false # disable to prevent sending hook task during unrelated tests
|
is_active: false # disable to prevent sending hook task during unrelated tests
|
||||||
|
|
|
@ -17,13 +17,17 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho
|
||||||
|
|
||||||
// Hook a hook is a web hook when one repository changed
|
// Hook a hook is a web hook when one repository changed
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
BranchFilter string `json:"branch_filter"`
|
BranchFilter string `json:"branch_filter"`
|
||||||
URL string `json:"-"`
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Deprecated: use Metadata instead
|
||||||
Config map[string]string `json:"config"`
|
Config map[string]string `json:"config"`
|
||||||
Events []string `json:"events"`
|
Events []string `json:"events"`
|
||||||
AuthorizationHeader string `json:"authorization_header"`
|
AuthorizationHeader string `json:"authorization_header"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
Metadata any `json:"metadata"`
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Updated time.Time `json:"updated_at"`
|
Updated time.Time `json:"updated_at"`
|
||||||
|
|
|
@ -637,17 +637,9 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["HookType"] = w.Type
|
ctx.Data["HookType"] = w.Type
|
||||||
switch w.Type {
|
|
||||||
case webhook_module.SLACK:
|
if handler := webhook_service.GetWebhookHandler(w.Type); handler != nil {
|
||||||
ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w)
|
ctx.Data["HookMetadata"] = handler.Metadata(w)
|
||||||
case webhook_module.DISCORD:
|
|
||||||
ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w)
|
|
||||||
case webhook_module.TELEGRAM:
|
|
||||||
ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w)
|
|
||||||
case webhook_module.MATRIX:
|
|
||||||
ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w)
|
|
||||||
case webhook_module.PACKAGIST:
|
|
||||||
ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["History"], err = w.History(ctx, 1)
|
ctx.Data["History"], err = w.History(ctx, 1)
|
||||||
|
|
136
services/webhook/default.go
Normal file
136
services/webhook/default.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Handler = defaultHandler{}
|
||||||
|
|
||||||
|
type defaultHandler struct {
|
||||||
|
forgejo bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dh defaultHandler) Type() webhook_module.HookType {
|
||||||
|
if dh.forgejo {
|
||||||
|
return webhook_module.FORGEJO
|
||||||
|
}
|
||||||
|
return webhook_module.GITEA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
|
func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
||||||
|
switch w.HTTPMethod {
|
||||||
|
case "":
|
||||||
|
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
|
||||||
|
fallthrough
|
||||||
|
case http.MethodPost:
|
||||||
|
switch w.ContentType {
|
||||||
|
case webhook_model.ContentTypeJSON:
|
||||||
|
req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
|
||||||
|
if err != nil {
|
||||||
|
return 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("POST", 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("GET", 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("PUT", 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), t, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDefaultHeaders(req *http.Request, secret []byte, 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)
|
||||||
|
req.Header.Add("X-Forgejo-Delivery", t.UUID)
|
||||||
|
req.Header.Add("X-Forgejo-Event", event)
|
||||||
|
req.Header.Add("X-Forgejo-Event-Type", eventType)
|
||||||
|
req.Header.Add("X-Forgejo-Signature", signatureSHA256)
|
||||||
|
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-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}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -5,11 +5,7 @@ package webhook
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -32,106 +28,6 @@ import (
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
|
||||||
switch w.HTTPMethod {
|
|
||||||
case "":
|
|
||||||
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
|
|
||||||
fallthrough
|
|
||||||
case http.MethodPost:
|
|
||||||
switch w.ContentType {
|
|
||||||
case webhook_model.ContentTypeJSON:
|
|
||||||
req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
|
|
||||||
if err != nil {
|
|
||||||
return 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("POST", 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("GET", 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("PUT", 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), t, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addDefaultHeaders(req *http.Request, secret []byte, 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)
|
|
||||||
req.Header.Add("X-Forgejo-Delivery", t.UUID)
|
|
||||||
req.Header.Add("X-Forgejo-Event", event)
|
|
||||||
req.Header.Add("X-Forgejo-Event-Type", eventType)
|
|
||||||
req.Header.Add("X-Forgejo-Signature", signatureSHA256)
|
|
||||||
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-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}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deliver creates the [http.Request] (depending on the webhook type), sends it
|
// Deliver creates the [http.Request] (depending on the webhook type), sends it
|
||||||
// and records the status and response.
|
// and records the status and response.
|
||||||
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
|
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
|
||||||
|
@ -151,12 +47,15 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
|
||||||
|
|
||||||
t.IsDelivered = true
|
t.IsDelivered = true
|
||||||
|
|
||||||
newRequest := webhookRequesters[w.Type]
|
handler := GetWebhookHandler(w.Type)
|
||||||
if t.PayloadVersion == 1 || newRequest == nil {
|
if handler == nil {
|
||||||
newRequest = newDefaultRequest
|
return fmt.Errorf("GetWebhookHandler %q", w.Type)
|
||||||
|
}
|
||||||
|
if t.PayloadVersion == 1 {
|
||||||
|
handler = defaultHandler{true}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, body, err := newRequest(ctx, w, t)
|
req, body, err := handler.NewRequest(ctx, w, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
|
return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,11 @@ import (
|
||||||
dingtalk "gitea.com/lunny/dingtalk_webhook"
|
dingtalk "gitea.com/lunny/dingtalk_webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type dingtalkHandler struct{}
|
||||||
|
|
||||||
|
func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK }
|
||||||
|
func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// DingtalkPayload represents
|
// DingtalkPayload represents
|
||||||
DingtalkPayload dingtalk.Payload
|
DingtalkPayload dingtalk.Payload
|
||||||
|
@ -190,6 +195,6 @@ type dingtalkConvertor struct{}
|
||||||
|
|
||||||
var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
|
var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
|
||||||
|
|
||||||
func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
return newJSONRequest(dingtalkConvertor{}, w, t, true)
|
return newJSONRequest(dingtalkConvertor{}, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,7 +236,7 @@ func TestDingTalkJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newDingtalkRequest(context.Background(), hook, task)
|
req, reqBody, err := dingtalkHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -22,6 +22,10 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type discordHandler struct{}
|
||||||
|
|
||||||
|
func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// DiscordEmbedFooter for Embed Footer Structure.
|
// DiscordEmbedFooter for Embed Footer Structure.
|
||||||
DiscordEmbedFooter struct {
|
DiscordEmbedFooter struct {
|
||||||
|
@ -69,11 +73,11 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDiscordHook returns discord metadata
|
// Metadata returns discord metadata
|
||||||
func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta {
|
func (discordHandler) Metadata(w *webhook_model.Webhook) any {
|
||||||
s := &DiscordMeta{}
|
s := &DiscordMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||||
log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
|
log.Error("discordHandler.Metadata(%d): %v", w.ID, err)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -260,10 +264,10 @@ type discordConvertor struct {
|
||||||
|
|
||||||
var _ payloadConvertor[DiscordPayload] = discordConvertor{}
|
var _ payloadConvertor[DiscordPayload] = discordConvertor{}
|
||||||
|
|
||||||
func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &DiscordMeta{}
|
meta := &DiscordMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||||
return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
|
return nil, nil, fmt.Errorf("discordHandler.NewRequest meta json: %w", err)
|
||||||
}
|
}
|
||||||
sc := discordConvertor{
|
sc := discordConvertor{
|
||||||
Username: meta.Username,
|
Username: meta.Username,
|
||||||
|
|
|
@ -275,7 +275,7 @@ func TestDiscordJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newDiscordRequest(context.Background(), hook, task)
|
req, reqBody, err := discordHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -15,6 +15,11 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type feishuHandler struct{}
|
||||||
|
|
||||||
|
func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU }
|
||||||
|
func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// FeishuPayload represents
|
// FeishuPayload represents
|
||||||
FeishuPayload struct {
|
FeishuPayload struct {
|
||||||
|
@ -168,6 +173,6 @@ type feishuConvertor struct{}
|
||||||
|
|
||||||
var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
|
var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
|
||||||
|
|
||||||
func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
return newJSONRequest(feishuConvertor{}, w, t, true)
|
return newJSONRequest(feishuConvertor{}, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ func TestFeishuJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
|
req, reqBody, err := feishuHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -314,33 +314,41 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
|
||||||
// ToHook convert models.Webhook to api.Hook
|
// ToHook convert models.Webhook to api.Hook
|
||||||
// This function is not part of the convert package to prevent an import cycle
|
// 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) {
|
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||||
|
// config is deprecated, but kept for compatibility
|
||||||
config := map[string]string{
|
config := map[string]string{
|
||||||
"url": w.URL,
|
"url": w.URL,
|
||||||
"content_type": w.ContentType.Name(),
|
"content_type": w.ContentType.Name(),
|
||||||
}
|
}
|
||||||
if w.Type == webhook_module.SLACK {
|
if w.Type == webhook_module.SLACK {
|
||||||
s := GetSlackHook(w)
|
if s, ok := (slackHandler{}.Metadata(w)).(*SlackMeta); ok {
|
||||||
config["channel"] = s.Channel
|
config["channel"] = s.Channel
|
||||||
config["username"] = s.Username
|
config["username"] = s.Username
|
||||||
config["icon_url"] = s.IconURL
|
config["icon_url"] = s.IconURL
|
||||||
config["color"] = s.Color
|
config["color"] = s.Color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizationHeader, err := w.HeaderAuthorization()
|
authorizationHeader, err := w.HeaderAuthorization()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var metadata any
|
||||||
|
if handler := GetWebhookHandler(w.Type); handler != nil {
|
||||||
|
metadata = handler.Metadata(w)
|
||||||
|
}
|
||||||
|
|
||||||
return &api.Hook{
|
return &api.Hook{
|
||||||
ID: w.ID,
|
ID: w.ID,
|
||||||
Type: w.Type,
|
Type: w.Type,
|
||||||
URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
|
BranchFilter: w.BranchFilter,
|
||||||
Active: w.IsActive,
|
URL: w.URL,
|
||||||
Config: config,
|
Config: config,
|
||||||
Events: w.EventsArray(),
|
Events: w.EventsArray(),
|
||||||
AuthorizationHeader: authorizationHeader,
|
AuthorizationHeader: authorizationHeader,
|
||||||
|
ContentType: w.ContentType.Name(),
|
||||||
|
Metadata: metadata,
|
||||||
|
Active: w.IsActive,
|
||||||
Updated: w.UpdatedUnix.AsTime(),
|
Updated: w.UpdatedUnix.AsTime(),
|
||||||
Created: w.CreatedUnix.AsTime(),
|
Created: w.CreatedUnix.AsTime(),
|
||||||
BranchFilter: w.BranchFilter,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
12
services/webhook/gogs.go
Normal file
12
services/webhook/gogs.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gogsHandler struct{ defaultHandler }
|
||||||
|
|
||||||
|
func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS }
|
|
@ -24,10 +24,14 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
type matrixHandler struct{}
|
||||||
|
|
||||||
|
func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX }
|
||||||
|
|
||||||
|
func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &MatrixMeta{}
|
meta := &MatrixMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||||
return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err)
|
return nil, nil, fmt.Errorf("matrixHandler.NewRequest meta json: %w", err)
|
||||||
}
|
}
|
||||||
mc := matrixConvertor{
|
mc := matrixConvertor{
|
||||||
MsgType: messageTypeText[meta.MessageType],
|
MsgType: messageTypeText[meta.MessageType],
|
||||||
|
@ -69,11 +73,11 @@ var messageTypeText = map[int]string{
|
||||||
2: "m.text",
|
2: "m.text",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMatrixHook returns Matrix metadata
|
// Metadata returns Matrix metadata
|
||||||
func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
|
func (matrixHandler) Metadata(w *webhook_model.Webhook) any {
|
||||||
s := &MatrixMeta{}
|
s := &MatrixMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||||
log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err)
|
log.Error("matrixHandler.Metadata(%d): %v", w.ID, err)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,7 +211,7 @@ func TestMatrixJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newMatrixRequest(context.Background(), hook, task)
|
req, reqBody, err := matrixHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -17,6 +17,11 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type msteamsHandler struct{}
|
||||||
|
|
||||||
|
func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS }
|
||||||
|
func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// MSTeamsFact for Fact Structure
|
// MSTeamsFact for Fact Structure
|
||||||
MSTeamsFact struct {
|
MSTeamsFact struct {
|
||||||
|
@ -347,6 +352,6 @@ type msteamsConvertor struct{}
|
||||||
|
|
||||||
var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
|
var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
|
||||||
|
|
||||||
func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
return newJSONRequest(msteamsConvertor{}, w, t, true)
|
return newJSONRequest(msteamsConvertor{}, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -439,7 +439,7 @@ func TestMSTeamsJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task)
|
req, reqBody, err := msteamsHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -11,8 +11,13 @@ import (
|
||||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type packagistHandler struct{}
|
||||||
|
|
||||||
|
func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// PackagistPayload represents a packagist payload
|
// PackagistPayload represents a packagist payload
|
||||||
// as expected by https://packagist.org/about
|
// as expected by https://packagist.org/about
|
||||||
|
@ -30,20 +35,20 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPackagistHook returns packagist metadata
|
// Metadata returns packagist metadata
|
||||||
func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
|
func (packagistHandler) Metadata(w *webhook_model.Webhook) any {
|
||||||
s := &PackagistMeta{}
|
s := &PackagistMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||||
log.Error("webhook.GetPackagistHook(%d): %v", w.ID, err)
|
log.Error("packagistHandler.Metadata(%d): %v", w.ID, err)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPackagistRequest creates a request with the [PackagistPayload] for packagist (same payload for all events).
|
// newPackagistRequest creates a request with the [PackagistPayload] for packagist (same payload for all events).
|
||||||
func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &PackagistMeta{}
|
meta := &PackagistMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||||
return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err)
|
return nil, nil, fmt.Errorf("packagistHandler.NewRequest meta json: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := PackagistPayload{
|
payload := PackagistPayload{
|
||||||
|
|
|
@ -53,7 +53,7 @@ func TestPackagistPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
|
req, reqBody, err := packagistHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -19,6 +19,10 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type slackHandler struct{}
|
||||||
|
|
||||||
|
func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK }
|
||||||
|
|
||||||
// SlackMeta contains the slack metadata
|
// SlackMeta contains the slack metadata
|
||||||
type SlackMeta struct {
|
type SlackMeta struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
|
@ -27,11 +31,11 @@ type SlackMeta struct {
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSlackHook returns slack metadata
|
// Metadata returns slack metadata
|
||||||
func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
|
func (slackHandler) Metadata(w *webhook_model.Webhook) any {
|
||||||
s := &SlackMeta{}
|
s := &SlackMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||||
log.Error("webhook.GetSlackHook(%d): %v", w.ID, err)
|
log.Error("slackHandler.Metadata(%d): %v", w.ID, err)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -283,10 +287,10 @@ type slackConvertor struct {
|
||||||
|
|
||||||
var _ payloadConvertor[SlackPayload] = slackConvertor{}
|
var _ payloadConvertor[SlackPayload] = slackConvertor{}
|
||||||
|
|
||||||
func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &SlackMeta{}
|
meta := &SlackMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
|
||||||
return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err)
|
return nil, nil, fmt.Errorf("slackHandler.NewRequest meta json: %w", err)
|
||||||
}
|
}
|
||||||
sc := slackConvertor{
|
sc := slackConvertor{
|
||||||
Channel: meta.Channel,
|
Channel: meta.Channel,
|
||||||
|
|
|
@ -178,7 +178,7 @@ func TestSlackJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newSlackRequest(context.Background(), hook, task)
|
req, reqBody, err := slackHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -211,3 +211,54 @@ func TestIsValidSlackChannel(t *testing.T) {
|
||||||
assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
|
assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlackMetadata(t *testing.T) {
|
||||||
|
w := &webhook_model.Webhook{
|
||||||
|
Meta: `{"channel": "foo", "username": "username", "color": "blue"}`,
|
||||||
|
}
|
||||||
|
slackHook := slackHandler{}.Metadata(w)
|
||||||
|
assert.Equal(t, *slackHook.(*SlackMeta), SlackMeta{
|
||||||
|
Channel: "foo",
|
||||||
|
Username: "username",
|
||||||
|
Color: "blue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlackToHook(t *testing.T) {
|
||||||
|
w := &webhook_model.Webhook{
|
||||||
|
Type: webhook_module.SLACK,
|
||||||
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
|
URL: "https://slack.example.com",
|
||||||
|
Meta: `{"channel": "foo", "username": "username", "color": "blue"}`,
|
||||||
|
HookEvent: &webhook_module.HookEvent{
|
||||||
|
PushOnly: true,
|
||||||
|
SendEverything: false,
|
||||||
|
ChooseEvents: false,
|
||||||
|
HookEvents: webhook_module.HookEvents{
|
||||||
|
Create: false,
|
||||||
|
Push: true,
|
||||||
|
PullRequest: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h, err := ToHook("repoLink", w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, h.Config, map[string]string{
|
||||||
|
"url": "https://slack.example.com",
|
||||||
|
"content_type": "json",
|
||||||
|
|
||||||
|
"channel": "foo",
|
||||||
|
"color": "blue",
|
||||||
|
"icon_url": "",
|
||||||
|
"username": "username",
|
||||||
|
})
|
||||||
|
assert.Equal(t, h.URL, "https://slack.example.com")
|
||||||
|
assert.Equal(t, h.ContentType, "json")
|
||||||
|
assert.Equal(t, h.Metadata, &SlackMeta{
|
||||||
|
Channel: "foo",
|
||||||
|
Username: "username",
|
||||||
|
IconURL: "",
|
||||||
|
Color: "blue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type telegramHandler struct{}
|
||||||
|
|
||||||
|
func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// TelegramPayload represents
|
// TelegramPayload represents
|
||||||
TelegramPayload struct {
|
TelegramPayload struct {
|
||||||
|
@ -33,11 +37,11 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTelegramHook returns telegram metadata
|
// Metadata returns telegram metadata
|
||||||
func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
|
func (telegramHandler) Metadata(w *webhook_model.Webhook) any {
|
||||||
s := &TelegramMeta{}
|
s := &TelegramMeta{}
|
||||||
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
|
||||||
log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err)
|
log.Error("telegramHandler.Metadata(%d): %v", w.ID, err)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -189,6 +193,6 @@ type telegramConvertor struct{}
|
||||||
|
|
||||||
var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
|
var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
|
||||||
|
|
||||||
func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
return newJSONRequest(telegramConvertor{}, w, t, true)
|
return newJSONRequest(telegramConvertor{}, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ func TestTelegramJSONPayload(t *testing.T) {
|
||||||
PayloadVersion: 2,
|
PayloadVersion: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, reqBody, err := newTelegramRequest(context.Background(), hook, task)
|
req, reqBody, err := telegramHandler{}.NewRequest(context.Background(), hook, task)
|
||||||
require.NotNil(t, req)
|
require.NotNil(t, req)
|
||||||
require.NotNil(t, reqBody)
|
require.NotNil(t, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -27,25 +27,46 @@ import (
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
)
|
)
|
||||||
|
|
||||||
var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
|
type Handler interface {
|
||||||
webhook_module.SLACK: newSlackRequest,
|
Type() webhook_module.HookType
|
||||||
webhook_module.DISCORD: newDiscordRequest,
|
NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
|
||||||
webhook_module.DINGTALK: newDingtalkRequest,
|
Metadata(*webhook_model.Webhook) any
|
||||||
webhook_module.TELEGRAM: newTelegramRequest,
|
}
|
||||||
webhook_module.MSTEAMS: newMSTeamsRequest,
|
|
||||||
webhook_module.FEISHU: newFeishuRequest,
|
var webhookHandlers = []Handler{
|
||||||
webhook_module.MATRIX: newMatrixRequest,
|
defaultHandler{true},
|
||||||
webhook_module.WECHATWORK: newWechatworkRequest,
|
defaultHandler{false},
|
||||||
webhook_module.PACKAGIST: newPackagistRequest,
|
gogsHandler{},
|
||||||
|
|
||||||
|
slackHandler{},
|
||||||
|
discordHandler{},
|
||||||
|
dingtalkHandler{},
|
||||||
|
telegramHandler{},
|
||||||
|
msteamsHandler{},
|
||||||
|
feishuHandler{},
|
||||||
|
matrixHandler{},
|
||||||
|
wechatworkHandler{},
|
||||||
|
packagistHandler{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWebhookHandler return the handler for a given webhook type (nil if not found)
|
||||||
|
func GetWebhookHandler(name webhook_module.HookType) Handler {
|
||||||
|
for _, h := range webhookHandlers {
|
||||||
|
if h.Type() == name {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List provides a list of the supported webhooks
|
||||||
|
func List() []Handler {
|
||||||
|
return webhookHandlers
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidHookTaskType returns true if a webhook registered
|
// IsValidHookTaskType returns true if a webhook registered
|
||||||
func IsValidHookTaskType(name string) bool {
|
func IsValidHookTaskType(name string) bool {
|
||||||
if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS {
|
return GetWebhookHandler(name) != nil
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, ok := webhookRequesters[name]
|
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hookQueue is a global queue of web hooks
|
// hookQueue is a global queue of web hooks
|
||||||
|
|
|
@ -16,18 +16,6 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebhook_GetSlackHook(t *testing.T) {
|
|
||||||
w := &webhook_model.Webhook{
|
|
||||||
Meta: `{"channel": "foo", "username": "username", "color": "blue"}`,
|
|
||||||
}
|
|
||||||
slackHook := GetSlackHook(w)
|
|
||||||
assert.Equal(t, *slackHook, SlackMeta{
|
|
||||||
Channel: "foo",
|
|
||||||
Username: "username",
|
|
||||||
Color: "blue",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func activateWebhook(t *testing.T, hookID int64) {
|
func activateWebhook(t *testing.T, hookID int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(webhook_model.Webhook{IsActive: true})
|
updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(webhook_model.Webhook{IsActive: true})
|
||||||
|
|
|
@ -15,6 +15,11 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type wechatworkHandler struct{}
|
||||||
|
|
||||||
|
func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK }
|
||||||
|
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// WechatworkPayload represents
|
// WechatworkPayload represents
|
||||||
WechatworkPayload struct {
|
WechatworkPayload struct {
|
||||||
|
@ -177,6 +182,6 @@ type wechatworkConvertor struct{}
|
||||||
|
|
||||||
var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
|
||||||
|
|
||||||
func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
return newJSONRequest(wechatworkConvertor{}, w, t, true)
|
return newJSONRequest(wechatworkConvertor{}, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="username">{{ctx.Locale.Tr "repo.settings.discord_username"}}</label>
|
<label for="username">{{ctx.Locale.Tr "repo.settings.discord_username"}}</label>
|
||||||
<input id="username" name="username" value="{{.DiscordHook.Username}}" placeholder="Forgejo">
|
<input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="icon_url">{{ctx.Locale.Tr "repo.settings.discord_icon_url"}}</label>
|
<label for="icon_url">{{ctx.Locale.Tr "repo.settings.discord_icon_url"}}</label>
|
||||||
<input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="https://example.com/assets/img/logo.svg">
|
<input id="icon_url" name="icon_url" value="{{.HookMetadata.IconURL}}" placeholder="https://example.com/assets/img/logo.svg">
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/settings/webhook/settings" .}}
|
{{template "repo/settings/webhook/settings" .}}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="required field {{if .Err_HomeserverURL}}error{{end}}">
|
<div class="required field {{if .Err_HomeserverURL}}error{{end}}">
|
||||||
<label for="homeserver_url">{{ctx.Locale.Tr "repo.settings.matrix.homeserver_url"}}</label>
|
<label for="homeserver_url">{{ctx.Locale.Tr "repo.settings.matrix.homeserver_url"}}</label>
|
||||||
<input id="homeserver_url" name="homeserver_url" type="url" value="{{.MatrixHook.HomeserverURL}}" autofocus required>
|
<input id="homeserver_url" name="homeserver_url" type="url" value="{{.HookMetadata.HomeserverURL}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_Room}}error{{end}}">
|
<div class="required field {{if .Err_Room}}error{{end}}">
|
||||||
<label for="room_id">{{ctx.Locale.Tr "repo.settings.matrix.room_id"}}</label>
|
<label for="room_id">{{ctx.Locale.Tr "repo.settings.matrix.room_id"}}</label>
|
||||||
<input id="room_id" name="room_id" type="text" value="{{.MatrixHook.Room}}" required>
|
<input id="room_id" name="room_id" type="text" value="{{.HookMetadata.Room}}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.matrix.message_type"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.matrix.message_type"}}</label>
|
||||||
<div class="ui selection dropdown">
|
<div class="ui selection dropdown">
|
||||||
<input type="hidden" id="message_type" name="message_type" value="{{if .MatrixHook.MessageType}}{{.MatrixHook.MessageType}}{{else}}1{{end}}">
|
<input type="hidden" id="message_type" name="message_type" value="{{if .HookMetadata.MessageType}}{{.HookMetadata.MessageType}}{{else}}1{{end}}">
|
||||||
<div class="default text"></div>
|
<div class="default text"></div>
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="required field {{if .Err_Username}}error{{end}}">
|
<div class="required field {{if .Err_Username}}error{{end}}">
|
||||||
<label for="username">{{ctx.Locale.Tr "repo.settings.packagist_username"}}</label>
|
<label for="username">{{ctx.Locale.Tr "repo.settings.packagist_username"}}</label>
|
||||||
<input id="username" name="username" value="{{.PackagistHook.Username}}" placeholder="Forgejo" autofocus required>
|
<input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_APIToken}}error{{end}}">
|
<div class="required field {{if .Err_APIToken}}error{{end}}">
|
||||||
<label for="api_token">{{ctx.Locale.Tr "repo.settings.packagist_api_token"}}</label>
|
<label for="api_token">{{ctx.Locale.Tr "repo.settings.packagist_api_token"}}</label>
|
||||||
<input id="api_token" name="api_token" value="{{.PackagistHook.APIToken}}" placeholder="X5F_tZ-Wj3c1vqaU2Rky" required>
|
<input id="api_token" name="api_token" value="{{.HookMetadata.APIToken}}" placeholder="X5F_tZ-Wj3c1vqaU2Rky" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_PackageURL}}error{{end}}">
|
<div class="required field {{if .Err_PackageURL}}error{{end}}">
|
||||||
<label for="package_url">{{ctx.Locale.Tr "repo.settings.packagist_package_url"}}</label>
|
<label for="package_url">{{ctx.Locale.Tr "repo.settings.packagist_package_url"}}</label>
|
||||||
<input id="package_url" name="package_url" value="{{.PackagistHook.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required>
|
<input id="package_url" name="package_url" value="{{.HookMetadata.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required>
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/settings/webhook/settings" .}}
|
{{template "repo/settings/webhook/settings" .}}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -8,20 +8,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_Channel}}error{{end}}">
|
<div class="required field {{if .Err_Channel}}error{{end}}">
|
||||||
<label for="channel">{{ctx.Locale.Tr "repo.settings.slack_channel"}}</label>
|
<label for="channel">{{ctx.Locale.Tr "repo.settings.slack_channel"}}</label>
|
||||||
<input id="channel" name="channel" value="{{.SlackHook.Channel}}" placeholder="#general" required>
|
<input id="channel" name="channel" value="{{.HookMetadata.Channel}}" placeholder="#general" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="username">{{ctx.Locale.Tr "repo.settings.slack_username"}}</label>
|
<label for="username">{{ctx.Locale.Tr "repo.settings.slack_username"}}</label>
|
||||||
<input id="username" name="username" value="{{.SlackHook.Username}}" placeholder="Forgejo">
|
<input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="icon_url">{{ctx.Locale.Tr "repo.settings.slack_icon_url"}}</label>
|
<label for="icon_url">{{ctx.Locale.Tr "repo.settings.slack_icon_url"}}</label>
|
||||||
<input id="icon_url" name="icon_url" value="{{.SlackHook.IconURL}}" placeholder="https://example.com/img/favicon.png">
|
<input id="icon_url" name="icon_url" value="{{.HookMetadata.IconURL}}" placeholder="https://example.com/img/favicon.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label>
|
<label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label>
|
||||||
<input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="#dd4b39, good, warning, danger">
|
<input id="color" name="color" value="{{.HookMetadata.Color}}" placeholder="#dd4b39, good, warning, danger">
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/settings/webhook/settings" .}}
|
{{template "repo/settings/webhook/settings" .}}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="required field {{if .Err_BotToken}}error{{end}}">
|
<div class="required field {{if .Err_BotToken}}error{{end}}">
|
||||||
<label for="bot_token">{{ctx.Locale.Tr "repo.settings.bot_token"}}</label>
|
<label for="bot_token">{{ctx.Locale.Tr "repo.settings.bot_token"}}</label>
|
||||||
<input id="bot_token" name="bot_token" type="text" value="{{.TelegramHook.BotToken}}" autofocus required>
|
<input id="bot_token" name="bot_token" type="text" value="{{.HookMetadata.BotToken}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="required field {{if .Err_ChatID}}error{{end}}">
|
<div class="required field {{if .Err_ChatID}}error{{end}}">
|
||||||
<label for="chat_id">{{ctx.Locale.Tr "repo.settings.chat_id"}}</label>
|
<label for="chat_id">{{ctx.Locale.Tr "repo.settings.chat_id"}}</label>
|
||||||
<input id="chat_id" name="chat_id" type="text" value="{{.TelegramHook.ChatID}}" required>
|
<input id="chat_id" name="chat_id" type="text" value="{{.HookMetadata.ChatID}}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="field {{if .Err_ThreadID}}error{{end}}">
|
<div class="field {{if .Err_ThreadID}}error{{end}}">
|
||||||
<label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label>
|
<label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label>
|
||||||
<input id="thread_id" name="thread_id" type="text" value="{{.TelegramHook.ThreadID}}">
|
<input id="thread_id" name="thread_id" type="text" value="{{.HookMetadata.ThreadID}}">
|
||||||
</div>
|
</div>
|
||||||
{{template "repo/settings/webhook/settings" .}}
|
{{template "repo/settings/webhook/settings" .}}
|
||||||
</form>
|
</form>
|
||||||
|
|
12
templates/swagger/v1_json.tmpl
generated
12
templates/swagger/v1_json.tmpl
generated
|
@ -20952,12 +20952,17 @@
|
||||||
"x-go-name": "BranchFilter"
|
"x-go-name": "BranchFilter"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"description": "Deprecated: use Metadata instead",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"x-go-name": "Config"
|
"x-go-name": "Config"
|
||||||
},
|
},
|
||||||
|
"content_type": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "ContentType"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
|
@ -20975,6 +20980,9 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "ID"
|
"x-go-name": "ID"
|
||||||
},
|
},
|
||||||
|
"metadata": {
|
||||||
|
"x-go-name": "Metadata"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Type"
|
"x-go-name": "Type"
|
||||||
|
@ -20983,6 +20991,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"x-go-name": "Updated"
|
"x-go-name": "Updated"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
|
@ -40,5 +40,6 @@ func TestAPICreateHook(t *testing.T) {
|
||||||
var apiHook *api.Hook
|
var apiHook *api.Hook
|
||||||
DecodeJSON(t, resp, &apiHook)
|
DecodeJSON(t, resp, &apiHook)
|
||||||
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
|
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
|
||||||
|
assert.Equal(t, "http://example.com/", apiHook.URL)
|
||||||
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
|
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/webhook"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
@ -21,7 +22,7 @@ func TestNewWebHookLink(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
webhooksLen := 12
|
webhooksLen := len(webhook.List())
|
||||||
baseurl := "/user2/repo1/settings/hooks"
|
baseurl := "/user2/repo1/settings/hooks"
|
||||||
tests := []string{
|
tests := []string{
|
||||||
// webhook list page
|
// webhook list page
|
||||||
|
|
Loading…
Reference in a new issue