[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"
 | 
			
		||||
	func NewNotifier
 | 
			
		||||
	func List
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
  repo_id: 1
 | 
			
		||||
  url: http://www.example.com/url1
 | 
			
		||||
  http_method: POST
 | 
			
		||||
  type: forgejo
 | 
			
		||||
  content_type: 1 # json
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
type Hook struct {
 | 
			
		||||
	ID                  int64             `json:"id"`
 | 
			
		||||
	Type                string            `json:"type"`
 | 
			
		||||
	BranchFilter        string            `json:"branch_filter"`
 | 
			
		||||
	URL                 string            `json:"-"`
 | 
			
		||||
	ID           int64  `json:"id"`
 | 
			
		||||
	Type         string `json:"type"`
 | 
			
		||||
	BranchFilter string `json:"branch_filter"`
 | 
			
		||||
	URL          string `json:"url"`
 | 
			
		||||
 | 
			
		||||
	// Deprecated: use Metadata instead
 | 
			
		||||
	Config              map[string]string `json:"config"`
 | 
			
		||||
	Events              []string          `json:"events"`
 | 
			
		||||
	AuthorizationHeader string            `json:"authorization_header"`
 | 
			
		||||
	ContentType         string            `json:"content_type"`
 | 
			
		||||
	Metadata            any               `json:"metadata"`
 | 
			
		||||
	Active              bool              `json:"active"`
 | 
			
		||||
	// swagger:strfmt date-time
 | 
			
		||||
	Updated time.Time `json:"updated_at"`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -637,17 +637,9 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["HookType"] = w.Type
 | 
			
		||||
	switch w.Type {
 | 
			
		||||
	case webhook_module.SLACK:
 | 
			
		||||
		ctx.Data["SlackHook"] = webhook_service.GetSlackHook(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)
 | 
			
		||||
 | 
			
		||||
	if handler := webhook_service.GetWebhookHandler(w.Type); handler != nil {
 | 
			
		||||
		ctx.Data["HookMetadata"] = handler.Metadata(w)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/hmac"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
| 
						 | 
				
			
			@ -32,106 +28,6 @@ import (
 | 
			
		|||
	"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
 | 
			
		||||
// and records the status and response.
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
	newRequest := webhookRequesters[w.Type]
 | 
			
		||||
	if t.PayloadVersion == 1 || newRequest == nil {
 | 
			
		||||
		newRequest = newDefaultRequest
 | 
			
		||||
	handler := GetWebhookHandler(w.Type)
 | 
			
		||||
	if handler == nil {
 | 
			
		||||
		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 {
 | 
			
		||||
		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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type dingtalkHandler struct{}
 | 
			
		||||
 | 
			
		||||
func (dingtalkHandler) Type() webhook_module.HookType       { return webhook_module.DINGTALK }
 | 
			
		||||
func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil }
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	// DingtalkPayload represents
 | 
			
		||||
	DingtalkPayload dingtalk.Payload
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +195,6 @@ type dingtalkConvertor struct{}
 | 
			
		|||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -236,7 +236,7 @@ func TestDingTalkJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,10 @@ import (
 | 
			
		|||
	webhook_module "code.gitea.io/gitea/modules/webhook"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type discordHandler struct{}
 | 
			
		||||
 | 
			
		||||
func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD }
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	// DiscordEmbedFooter for Embed Footer Structure.
 | 
			
		||||
	DiscordEmbedFooter struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,11 +73,11 @@ type (
 | 
			
		|||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetDiscordHook returns discord metadata
 | 
			
		||||
func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta {
 | 
			
		||||
// Metadata returns discord metadata
 | 
			
		||||
func (discordHandler) Metadata(w *webhook_model.Webhook) any {
 | 
			
		||||
	s := &DiscordMeta{}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -260,10 +264,10 @@ type discordConvertor struct {
 | 
			
		|||
 | 
			
		||||
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{}
 | 
			
		||||
	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{
 | 
			
		||||
		Username:  meta.Username,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -275,7 +275,7 @@ func TestDiscordJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,11 @@ import (
 | 
			
		|||
	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 (
 | 
			
		||||
	// FeishuPayload represents
 | 
			
		||||
	FeishuPayload struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -168,6 +173,6 @@ type feishuConvertor struct{}
 | 
			
		|||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,7 +177,7 @@ func TestFeishuJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -314,33 +314,41 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
 | 
			
		|||
// ToHook convert models.Webhook to api.Hook
 | 
			
		||||
// This function is not part of the convert package to prevent an import cycle
 | 
			
		||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
 | 
			
		||||
	// config is deprecated, but kept for compatibility
 | 
			
		||||
	config := map[string]string{
 | 
			
		||||
		"url":          w.URL,
 | 
			
		||||
		"content_type": w.ContentType.Name(),
 | 
			
		||||
	}
 | 
			
		||||
	if w.Type == webhook_module.SLACK {
 | 
			
		||||
		s := GetSlackHook(w)
 | 
			
		||||
		config["channel"] = s.Channel
 | 
			
		||||
		config["username"] = s.Username
 | 
			
		||||
		config["icon_url"] = s.IconURL
 | 
			
		||||
		config["color"] = s.Color
 | 
			
		||||
		if s, ok := (slackHandler{}.Metadata(w)).(*SlackMeta); ok {
 | 
			
		||||
			config["channel"] = s.Channel
 | 
			
		||||
			config["username"] = s.Username
 | 
			
		||||
			config["icon_url"] = s.IconURL
 | 
			
		||||
			config["color"] = s.Color
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authorizationHeader, err := w.HeaderAuthorization()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	var metadata any
 | 
			
		||||
	if handler := GetWebhookHandler(w.Type); handler != nil {
 | 
			
		||||
		metadata = handler.Metadata(w)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &api.Hook{
 | 
			
		||||
		ID:                  w.ID,
 | 
			
		||||
		Type:                w.Type,
 | 
			
		||||
		URL:                 fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
 | 
			
		||||
		Active:              w.IsActive,
 | 
			
		||||
		BranchFilter:        w.BranchFilter,
 | 
			
		||||
		URL:                 w.URL,
 | 
			
		||||
		Config:              config,
 | 
			
		||||
		Events:              w.EventsArray(),
 | 
			
		||||
		AuthorizationHeader: authorizationHeader,
 | 
			
		||||
		ContentType:         w.ContentType.Name(),
 | 
			
		||||
		Metadata:            metadata,
 | 
			
		||||
		Active:              w.IsActive,
 | 
			
		||||
		Updated:             w.UpdatedUnix.AsTime(),
 | 
			
		||||
		Created:             w.CreatedUnix.AsTime(),
 | 
			
		||||
		BranchFilter:        w.BranchFilter,
 | 
			
		||||
	}, 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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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{}
 | 
			
		||||
	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{
 | 
			
		||||
		MsgType: messageTypeText[meta.MessageType],
 | 
			
		||||
| 
						 | 
				
			
			@ -69,11 +73,11 @@ var messageTypeText = map[int]string{
 | 
			
		|||
	2: "m.text",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMatrixHook returns Matrix metadata
 | 
			
		||||
func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
 | 
			
		||||
// Metadata returns Matrix metadata
 | 
			
		||||
func (matrixHandler) Metadata(w *webhook_model.Webhook) any {
 | 
			
		||||
	s := &MatrixMeta{}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -211,7 +211,7 @@ func TestMatrixJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,11 @@ import (
 | 
			
		|||
	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 (
 | 
			
		||||
	// MSTeamsFact for Fact Structure
 | 
			
		||||
	MSTeamsFact struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -347,6 +352,6 @@ type msteamsConvertor struct{}
 | 
			
		|||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -439,7 +439,7 @@ func TestMSTeamsJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,8 +11,13 @@ import (
 | 
			
		|||
	webhook_model "code.gitea.io/gitea/models/webhook"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"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 (
 | 
			
		||||
	// PackagistPayload represents a packagist payload
 | 
			
		||||
	// as expected by https://packagist.org/about
 | 
			
		||||
| 
						 | 
				
			
			@ -30,20 +35,20 @@ type (
 | 
			
		|||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetPackagistHook returns packagist metadata
 | 
			
		||||
func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
 | 
			
		||||
// Metadata returns packagist metadata
 | 
			
		||||
func (packagistHandler) Metadata(w *webhook_model.Webhook) any {
 | 
			
		||||
	s := &PackagistMeta{}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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{}
 | 
			
		||||
	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{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ func TestPackagistPayload(t *testing.T) {
 | 
			
		|||
				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, reqBody)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,10 @@ import (
 | 
			
		|||
	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
 | 
			
		||||
type SlackMeta struct {
 | 
			
		||||
	Channel  string `json:"channel"`
 | 
			
		||||
| 
						 | 
				
			
			@ -27,11 +31,11 @@ type SlackMeta struct {
 | 
			
		|||
	Color    string `json:"color"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSlackHook returns slack metadata
 | 
			
		||||
func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
 | 
			
		||||
// Metadata returns slack metadata
 | 
			
		||||
func (slackHandler) Metadata(w *webhook_model.Webhook) any {
 | 
			
		||||
	s := &SlackMeta{}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -283,10 +287,10 @@ type slackConvertor struct {
 | 
			
		|||
 | 
			
		||||
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{}
 | 
			
		||||
	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{
 | 
			
		||||
		Channel:  meta.Channel,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -178,7 +178,7 @@ func TestSlackJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -211,3 +211,54 @@ func TestIsValidSlackChannel(t *testing.T) {
 | 
			
		|||
		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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type telegramHandler struct{}
 | 
			
		||||
 | 
			
		||||
func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM }
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	// TelegramPayload represents
 | 
			
		||||
	TelegramPayload struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,11 +37,11 @@ type (
 | 
			
		|||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetTelegramHook returns telegram metadata
 | 
			
		||||
func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
 | 
			
		||||
// Metadata returns telegram metadata
 | 
			
		||||
func (telegramHandler) Metadata(w *webhook_model.Webhook) any {
 | 
			
		||||
	s := &TelegramMeta{}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -189,6 +193,6 @@ type telegramConvertor struct{}
 | 
			
		|||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,7 +177,7 @@ func TestTelegramJSONPayload(t *testing.T) {
 | 
			
		|||
		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, reqBody)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,25 +27,46 @@ import (
 | 
			
		|||
	"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){
 | 
			
		||||
	webhook_module.SLACK:      newSlackRequest,
 | 
			
		||||
	webhook_module.DISCORD:    newDiscordRequest,
 | 
			
		||||
	webhook_module.DINGTALK:   newDingtalkRequest,
 | 
			
		||||
	webhook_module.TELEGRAM:   newTelegramRequest,
 | 
			
		||||
	webhook_module.MSTEAMS:    newMSTeamsRequest,
 | 
			
		||||
	webhook_module.FEISHU:     newFeishuRequest,
 | 
			
		||||
	webhook_module.MATRIX:     newMatrixRequest,
 | 
			
		||||
	webhook_module.WECHATWORK: newWechatworkRequest,
 | 
			
		||||
	webhook_module.PACKAGIST:  newPackagistRequest,
 | 
			
		||||
type Handler interface {
 | 
			
		||||
	Type() webhook_module.HookType
 | 
			
		||||
	NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
 | 
			
		||||
	Metadata(*webhook_model.Webhook) any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var webhookHandlers = []Handler{
 | 
			
		||||
	defaultHandler{true},
 | 
			
		||||
	defaultHandler{false},
 | 
			
		||||
	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
 | 
			
		||||
func IsValidHookTaskType(name string) bool {
 | 
			
		||||
	if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	_, ok := webhookRequesters[name]
 | 
			
		||||
	return ok
 | 
			
		||||
	return GetWebhookHandler(name) != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// hookQueue is a global queue of web hooks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,18 +16,6 @@ import (
 | 
			
		|||
	"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) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
	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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type wechatworkHandler struct{}
 | 
			
		||||
 | 
			
		||||
func (wechatworkHandler) Type() webhook_module.HookType       { return webhook_module.WECHATWORK }
 | 
			
		||||
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
	// WechatworkPayload represents
 | 
			
		||||
	WechatworkPayload struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -177,6 +182,6 @@ type wechatworkConvertor struct{}
 | 
			
		|||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,11 +8,11 @@
 | 
			
		|||
		</div>
 | 
			
		||||
		<div class="field">
 | 
			
		||||
			<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 class="field">
 | 
			
		||||
			<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>
 | 
			
		||||
		{{template "repo/settings/webhook/settings" .}}
 | 
			
		||||
	</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,16 @@
 | 
			
		|||
		{{.CsrfTokenHtml}}
 | 
			
		||||
		<div class="required field {{if .Err_HomeserverURL}}error{{end}}">
 | 
			
		||||
			<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 class="required field {{if .Err_Room}}error{{end}}">
 | 
			
		||||
			<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 class="field">
 | 
			
		||||
			<label>{{ctx.Locale.Tr "repo.settings.matrix.message_type"}}</label>
 | 
			
		||||
				<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>
 | 
			
		||||
				{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
			
		||||
				<div class="menu">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,15 +4,15 @@
 | 
			
		|||
		{{.CsrfTokenHtml}}
 | 
			
		||||
		<div class="required field {{if .Err_Username}}error{{end}}">
 | 
			
		||||
			<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 class="required field {{if .Err_APIToken}}error{{end}}">
 | 
			
		||||
			<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 class="required field {{if .Err_PackageURL}}error{{end}}">
 | 
			
		||||
			<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>
 | 
			
		||||
		{{template "repo/settings/webhook/settings" .}}
 | 
			
		||||
	</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,20 +8,20 @@
 | 
			
		|||
		</div>
 | 
			
		||||
		<div class="required field {{if .Err_Channel}}error{{end}}">
 | 
			
		||||
			<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 class="field">
 | 
			
		||||
			<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 class="field">
 | 
			
		||||
			<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 class="field">
 | 
			
		||||
			<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>
 | 
			
		||||
		{{template "repo/settings/webhook/settings" .}}
 | 
			
		||||
	</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,15 +4,15 @@
 | 
			
		|||
		{{.CsrfTokenHtml}}
 | 
			
		||||
		<div class="required field {{if .Err_BotToken}}error{{end}}">
 | 
			
		||||
			<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 class="required field {{if .Err_ChatID}}error{{end}}">
 | 
			
		||||
			<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 class="field {{if .Err_ThreadID}}error{{end}}">
 | 
			
		||||
			<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>
 | 
			
		||||
		{{template "repo/settings/webhook/settings" .}}
 | 
			
		||||
	</form>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								templates/swagger/v1_json.tmpl
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								templates/swagger/v1_json.tmpl
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -20952,12 +20952,17 @@
 | 
			
		|||
          "x-go-name": "BranchFilter"
 | 
			
		||||
        },
 | 
			
		||||
        "config": {
 | 
			
		||||
          "description": "Deprecated: use Metadata instead",
 | 
			
		||||
          "type": "object",
 | 
			
		||||
          "additionalProperties": {
 | 
			
		||||
            "type": "string"
 | 
			
		||||
          },
 | 
			
		||||
          "x-go-name": "Config"
 | 
			
		||||
        },
 | 
			
		||||
        "content_type": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "ContentType"
 | 
			
		||||
        },
 | 
			
		||||
        "created_at": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "format": "date-time",
 | 
			
		||||
| 
						 | 
				
			
			@ -20975,6 +20980,9 @@
 | 
			
		|||
          "format": "int64",
 | 
			
		||||
          "x-go-name": "ID"
 | 
			
		||||
        },
 | 
			
		||||
        "metadata": {
 | 
			
		||||
          "x-go-name": "Metadata"
 | 
			
		||||
        },
 | 
			
		||||
        "type": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "Type"
 | 
			
		||||
| 
						 | 
				
			
			@ -20983,6 +20991,10 @@
 | 
			
		|||
          "type": "string",
 | 
			
		||||
          "format": "date-time",
 | 
			
		||||
          "x-go-name": "Updated"
 | 
			
		||||
        },
 | 
			
		||||
        "url": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "URL"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,5 +40,6 @@ func TestAPICreateHook(t *testing.T) {
 | 
			
		|||
	var apiHook *api.Hook
 | 
			
		||||
	DecodeJSON(t, resp, &apiHook)
 | 
			
		||||
	assert.Equal(t, "http://example.com/", apiHook.Config["url"])
 | 
			
		||||
	assert.Equal(t, "http://example.com/", apiHook.URL)
 | 
			
		||||
	assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
	"testing"
 | 
			
		||||
 | 
			
		||||
	gitea_context "code.gitea.io/gitea/services/context"
 | 
			
		||||
	"code.gitea.io/gitea/services/webhook"
 | 
			
		||||
	"code.gitea.io/gitea/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/goquery"
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +22,7 @@ func TestNewWebHookLink(t *testing.T) {
 | 
			
		|||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
 | 
			
		||||
	webhooksLen := 12
 | 
			
		||||
	webhooksLen := len(webhook.List())
 | 
			
		||||
	baseurl := "/user2/repo1/settings/hooks"
 | 
			
		||||
	tests := []string{
 | 
			
		||||
		// webhook list page
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue