loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[REFACTOR] webhook.Handler interface' (#2758) from oliverpool/forgejo:webhook_2_interface into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2758
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+378 -210
+1
.deadcode-out
··· 341 341 342 342 package "code.gitea.io/gitea/services/webhook" 343 343 func NewNotifier 344 + func List 344 345
+1
models/fixtures/webhook.yml
··· 3 3 repo_id: 1 4 4 url: http://www.example.com/url1 5 5 http_method: POST 6 + type: forgejo 6 7 content_type: 1 # json 7 8 events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}' 8 9 is_active: false # disable to prevent sending hook task during unrelated tests
+8 -4
modules/structs/hook.go
··· 17 17 18 18 // Hook a hook is a web hook when one repository changed 19 19 type Hook struct { 20 - ID int64 `json:"id"` 21 - Type string `json:"type"` 22 - BranchFilter string `json:"branch_filter"` 23 - URL string `json:"-"` 20 + ID int64 `json:"id"` 21 + Type string `json:"type"` 22 + BranchFilter string `json:"branch_filter"` 23 + URL string `json:"url"` 24 + 25 + // Deprecated: use Metadata instead 24 26 Config map[string]string `json:"config"` 25 27 Events []string `json:"events"` 26 28 AuthorizationHeader string `json:"authorization_header"` 29 + ContentType string `json:"content_type"` 30 + Metadata any `json:"metadata"` 27 31 Active bool `json:"active"` 28 32 // swagger:strfmt date-time 29 33 Updated time.Time `json:"updated_at"`
+3 -11
routers/web/repo/setting/webhook.go
··· 637 637 } 638 638 639 639 ctx.Data["HookType"] = w.Type 640 - switch w.Type { 641 - case webhook_module.SLACK: 642 - ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w) 643 - case webhook_module.DISCORD: 644 - ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w) 645 - case webhook_module.TELEGRAM: 646 - ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w) 647 - case webhook_module.MATRIX: 648 - ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w) 649 - case webhook_module.PACKAGIST: 650 - ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w) 640 + 641 + if handler := webhook_service.GetWebhookHandler(w.Type); handler != nil { 642 + ctx.Data["HookMetadata"] = handler.Metadata(w) 651 643 } 652 644 653 645 ctx.Data["History"], err = w.History(ctx, 1)
+136
services/webhook/default.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package webhook 5 + 6 + import ( 7 + "context" 8 + "crypto/hmac" 9 + "crypto/sha1" 10 + "crypto/sha256" 11 + "encoding/hex" 12 + "fmt" 13 + "io" 14 + "net/http" 15 + "net/url" 16 + "strings" 17 + 18 + webhook_model "code.gitea.io/gitea/models/webhook" 19 + "code.gitea.io/gitea/modules/log" 20 + webhook_module "code.gitea.io/gitea/modules/webhook" 21 + ) 22 + 23 + var _ Handler = defaultHandler{} 24 + 25 + type defaultHandler struct { 26 + forgejo bool 27 + } 28 + 29 + func (dh defaultHandler) Type() webhook_module.HookType { 30 + if dh.forgejo { 31 + return webhook_module.FORGEJO 32 + } 33 + return webhook_module.GITEA 34 + } 35 + 36 + func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } 37 + 38 + func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { 39 + switch w.HTTPMethod { 40 + case "": 41 + log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) 42 + fallthrough 43 + case http.MethodPost: 44 + switch w.ContentType { 45 + case webhook_model.ContentTypeJSON: 46 + req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) 47 + if err != nil { 48 + return nil, nil, err 49 + } 50 + 51 + req.Header.Set("Content-Type", "application/json") 52 + case webhook_model.ContentTypeForm: 53 + forms := url.Values{ 54 + "payload": []string{t.PayloadContent}, 55 + } 56 + 57 + req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) 58 + if err != nil { 59 + return nil, nil, err 60 + } 61 + 62 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 63 + default: 64 + return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) 65 + } 66 + case http.MethodGet: 67 + u, err := url.Parse(w.URL) 68 + if err != nil { 69 + return nil, nil, fmt.Errorf("invalid URL: %w", err) 70 + } 71 + vals := u.Query() 72 + vals["payload"] = []string{t.PayloadContent} 73 + u.RawQuery = vals.Encode() 74 + req, err = http.NewRequest("GET", u.String(), nil) 75 + if err != nil { 76 + return nil, nil, err 77 + } 78 + case http.MethodPut: 79 + switch w.Type { 80 + case webhook_module.MATRIX: // used when t.Version == 1 81 + txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) 82 + if err != nil { 83 + return nil, nil, err 84 + } 85 + url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) 86 + req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) 87 + if err != nil { 88 + return nil, nil, err 89 + } 90 + default: 91 + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) 92 + } 93 + default: 94 + return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) 95 + } 96 + 97 + body = []byte(t.PayloadContent) 98 + return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) 99 + } 100 + 101 + func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { 102 + var signatureSHA1 string 103 + var signatureSHA256 string 104 + if len(secret) > 0 { 105 + sig1 := hmac.New(sha1.New, secret) 106 + sig256 := hmac.New(sha256.New, secret) 107 + _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) 108 + if err != nil { 109 + // this error should never happen, since the hashes are writing to []byte and always return a nil error. 110 + return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) 111 + } 112 + signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) 113 + signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) 114 + } 115 + 116 + event := t.EventType.Event() 117 + eventType := string(t.EventType) 118 + req.Header.Add("X-Forgejo-Delivery", t.UUID) 119 + req.Header.Add("X-Forgejo-Event", event) 120 + req.Header.Add("X-Forgejo-Event-Type", eventType) 121 + req.Header.Add("X-Forgejo-Signature", signatureSHA256) 122 + req.Header.Add("X-Gitea-Delivery", t.UUID) 123 + req.Header.Add("X-Gitea-Event", event) 124 + req.Header.Add("X-Gitea-Event-Type", eventType) 125 + req.Header.Add("X-Gitea-Signature", signatureSHA256) 126 + req.Header.Add("X-Gogs-Delivery", t.UUID) 127 + req.Header.Add("X-Gogs-Event", event) 128 + req.Header.Add("X-Gogs-Event-Type", eventType) 129 + req.Header.Add("X-Gogs-Signature", signatureSHA256) 130 + req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) 131 + req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) 132 + req.Header["X-GitHub-Delivery"] = []string{t.UUID} 133 + req.Header["X-GitHub-Event"] = []string{event} 134 + req.Header["X-GitHub-Event-Type"] = []string{eventType} 135 + return nil 136 + }
+7 -108
services/webhook/deliver.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "crypto/hmac" 9 - "crypto/sha1" 10 - "crypto/sha256" 11 8 "crypto/tls" 12 - "encoding/hex" 13 9 "fmt" 14 10 "io" 15 11 "net/http" ··· 32 28 "github.com/gobwas/glob" 33 29 ) 34 30 35 - func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { 36 - switch w.HTTPMethod { 37 - case "": 38 - log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) 39 - fallthrough 40 - case http.MethodPost: 41 - switch w.ContentType { 42 - case webhook_model.ContentTypeJSON: 43 - req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent)) 44 - if err != nil { 45 - return nil, nil, err 46 - } 47 - 48 - req.Header.Set("Content-Type", "application/json") 49 - case webhook_model.ContentTypeForm: 50 - forms := url.Values{ 51 - "payload": []string{t.PayloadContent}, 52 - } 53 - 54 - req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode())) 55 - if err != nil { 56 - return nil, nil, err 57 - } 58 - 59 - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 60 - default: 61 - return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType) 62 - } 63 - case http.MethodGet: 64 - u, err := url.Parse(w.URL) 65 - if err != nil { 66 - return nil, nil, fmt.Errorf("invalid URL: %w", err) 67 - } 68 - vals := u.Query() 69 - vals["payload"] = []string{t.PayloadContent} 70 - u.RawQuery = vals.Encode() 71 - req, err = http.NewRequest("GET", u.String(), nil) 72 - if err != nil { 73 - return nil, nil, err 74 - } 75 - case http.MethodPut: 76 - switch w.Type { 77 - case webhook_module.MATRIX: // used when t.Version == 1 78 - txnID, err := getMatrixTxnID([]byte(t.PayloadContent)) 79 - if err != nil { 80 - return nil, nil, err 81 - } 82 - url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID)) 83 - req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent)) 84 - if err != nil { 85 - return nil, nil, err 86 - } 87 - default: 88 - return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) 89 - } 90 - default: 91 - return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod) 92 - } 93 - 94 - body = []byte(t.PayloadContent) 95 - return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) 96 - } 97 - 98 - func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { 99 - var signatureSHA1 string 100 - var signatureSHA256 string 101 - if len(secret) > 0 { 102 - sig1 := hmac.New(sha1.New, secret) 103 - sig256 := hmac.New(sha256.New, secret) 104 - _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) 105 - if err != nil { 106 - // this error should never happen, since the hashes are writing to []byte and always return a nil error. 107 - return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) 108 - } 109 - signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) 110 - signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) 111 - } 112 - 113 - event := t.EventType.Event() 114 - eventType := string(t.EventType) 115 - req.Header.Add("X-Forgejo-Delivery", t.UUID) 116 - req.Header.Add("X-Forgejo-Event", event) 117 - req.Header.Add("X-Forgejo-Event-Type", eventType) 118 - req.Header.Add("X-Forgejo-Signature", signatureSHA256) 119 - req.Header.Add("X-Gitea-Delivery", t.UUID) 120 - req.Header.Add("X-Gitea-Event", event) 121 - req.Header.Add("X-Gitea-Event-Type", eventType) 122 - req.Header.Add("X-Gitea-Signature", signatureSHA256) 123 - req.Header.Add("X-Gogs-Delivery", t.UUID) 124 - req.Header.Add("X-Gogs-Event", event) 125 - req.Header.Add("X-Gogs-Event-Type", eventType) 126 - req.Header.Add("X-Gogs-Signature", signatureSHA256) 127 - req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) 128 - req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) 129 - req.Header["X-GitHub-Delivery"] = []string{t.UUID} 130 - req.Header["X-GitHub-Event"] = []string{event} 131 - req.Header["X-GitHub-Event-Type"] = []string{eventType} 132 - return nil 133 - } 134 - 135 31 // Deliver creates the [http.Request] (depending on the webhook type), sends it 136 32 // and records the status and response. 137 33 func Deliver(ctx context.Context, t *webhook_model.HookTask) error { ··· 151 47 152 48 t.IsDelivered = true 153 49 154 - newRequest := webhookRequesters[w.Type] 155 - if t.PayloadVersion == 1 || newRequest == nil { 156 - newRequest = newDefaultRequest 50 + handler := GetWebhookHandler(w.Type) 51 + if handler == nil { 52 + return fmt.Errorf("GetWebhookHandler %q", w.Type) 53 + } 54 + if t.PayloadVersion == 1 { 55 + handler = defaultHandler{true} 157 56 } 158 57 159 - req, body, err := newRequest(ctx, w, t) 58 + req, body, err := handler.NewRequest(ctx, w, t) 160 59 if err != nil { 161 60 return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err) 162 61 }
+6 -1
services/webhook/dingtalk.go
··· 19 19 dingtalk "gitea.com/lunny/dingtalk_webhook" 20 20 ) 21 21 22 + type dingtalkHandler struct{} 23 + 24 + func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } 25 + func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } 26 + 22 27 type ( 23 28 // DingtalkPayload represents 24 29 DingtalkPayload dingtalk.Payload ··· 190 195 191 196 var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} 192 197 193 - func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 198 + func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 194 199 return newJSONRequest(dingtalkConvertor{}, w, t, true) 195 200 }
+1 -1
services/webhook/dingtalk_test.go
··· 236 236 PayloadVersion: 2, 237 237 } 238 238 239 - req, reqBody, err := newDingtalkRequest(context.Background(), hook, task) 239 + req, reqBody, err := dingtalkHandler{}.NewRequest(context.Background(), hook, task) 240 240 require.NotNil(t, req) 241 241 require.NotNil(t, reqBody) 242 242 require.NoError(t, err)
+9 -5
services/webhook/discord.go
··· 22 22 webhook_module "code.gitea.io/gitea/modules/webhook" 23 23 ) 24 24 25 + type discordHandler struct{} 26 + 27 + func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } 28 + 25 29 type ( 26 30 // DiscordEmbedFooter for Embed Footer Structure. 27 31 DiscordEmbedFooter struct { ··· 69 73 } 70 74 ) 71 75 72 - // GetDiscordHook returns discord metadata 73 - func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta { 76 + // Metadata returns discord metadata 77 + func (discordHandler) Metadata(w *webhook_model.Webhook) any { 74 78 s := &DiscordMeta{} 75 79 if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 76 - log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err) 80 + log.Error("discordHandler.Metadata(%d): %v", w.ID, err) 77 81 } 78 82 return s 79 83 } ··· 260 264 261 265 var _ payloadConvertor[DiscordPayload] = discordConvertor{} 262 266 263 - func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 267 + func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 264 268 meta := &DiscordMeta{} 265 269 if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { 266 - return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err) 270 + return nil, nil, fmt.Errorf("discordHandler.NewRequest meta json: %w", err) 267 271 } 268 272 sc := discordConvertor{ 269 273 Username: meta.Username,
+1 -1
services/webhook/discord_test.go
··· 275 275 PayloadVersion: 2, 276 276 } 277 277 278 - req, reqBody, err := newDiscordRequest(context.Background(), hook, task) 278 + req, reqBody, err := discordHandler{}.NewRequest(context.Background(), hook, task) 279 279 require.NotNil(t, req) 280 280 require.NotNil(t, reqBody) 281 281 require.NoError(t, err)
+6 -1
services/webhook/feishu.go
··· 15 15 webhook_module "code.gitea.io/gitea/modules/webhook" 16 16 ) 17 17 18 + type feishuHandler struct{} 19 + 20 + func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } 21 + func (feishuHandler) Metadata(*webhook_model.Webhook) any { return nil } 22 + 18 23 type ( 19 24 // FeishuPayload represents 20 25 FeishuPayload struct { ··· 168 173 169 174 var _ payloadConvertor[FeishuPayload] = feishuConvertor{} 170 175 171 - func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 176 + func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 172 177 return newJSONRequest(feishuConvertor{}, w, t, true) 173 178 }
+1 -1
services/webhook/feishu_test.go
··· 177 177 PayloadVersion: 2, 178 178 } 179 179 180 - req, reqBody, err := newFeishuRequest(context.Background(), hook, task) 180 + req, reqBody, err := feishuHandler{}.NewRequest(context.Background(), hook, task) 181 181 require.NotNil(t, req) 182 182 require.NotNil(t, reqBody) 183 183 require.NoError(t, err)
+16 -8
services/webhook/general.go
··· 314 314 // ToHook convert models.Webhook to api.Hook 315 315 // This function is not part of the convert package to prevent an import cycle 316 316 func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { 317 + // config is deprecated, but kept for compatibility 317 318 config := map[string]string{ 318 319 "url": w.URL, 319 320 "content_type": w.ContentType.Name(), 320 321 } 321 322 if w.Type == webhook_module.SLACK { 322 - s := GetSlackHook(w) 323 - config["channel"] = s.Channel 324 - config["username"] = s.Username 325 - config["icon_url"] = s.IconURL 326 - config["color"] = s.Color 323 + if s, ok := (slackHandler{}.Metadata(w)).(*SlackMeta); ok { 324 + config["channel"] = s.Channel 325 + config["username"] = s.Username 326 + config["icon_url"] = s.IconURL 327 + config["color"] = s.Color 328 + } 327 329 } 328 330 329 331 authorizationHeader, err := w.HeaderAuthorization() 330 332 if err != nil { 331 333 return nil, err 332 334 } 335 + var metadata any 336 + if handler := GetWebhookHandler(w.Type); handler != nil { 337 + metadata = handler.Metadata(w) 338 + } 333 339 334 340 return &api.Hook{ 335 341 ID: w.ID, 336 342 Type: w.Type, 337 - URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), 338 - Active: w.IsActive, 343 + BranchFilter: w.BranchFilter, 344 + URL: w.URL, 339 345 Config: config, 340 346 Events: w.EventsArray(), 341 347 AuthorizationHeader: authorizationHeader, 348 + ContentType: w.ContentType.Name(), 349 + Metadata: metadata, 350 + Active: w.IsActive, 342 351 Updated: w.UpdatedUnix.AsTime(), 343 352 Created: w.CreatedUnix.AsTime(), 344 - BranchFilter: w.BranchFilter, 345 353 }, nil 346 354 }
+12
services/webhook/gogs.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package webhook 5 + 6 + import ( 7 + webhook_module "code.gitea.io/gitea/modules/webhook" 8 + ) 9 + 10 + type gogsHandler struct{ defaultHandler } 11 + 12 + func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS }
+9 -5
services/webhook/matrix.go
··· 24 24 webhook_module "code.gitea.io/gitea/modules/webhook" 25 25 ) 26 26 27 - func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 27 + type matrixHandler struct{} 28 + 29 + func (matrixHandler) Type() webhook_module.HookType { return webhook_module.MATRIX } 30 + 31 + func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 28 32 meta := &MatrixMeta{} 29 33 if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { 30 - return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err) 34 + return nil, nil, fmt.Errorf("matrixHandler.NewRequest meta json: %w", err) 31 35 } 32 36 mc := matrixConvertor{ 33 37 MsgType: messageTypeText[meta.MessageType], ··· 69 73 2: "m.text", 70 74 } 71 75 72 - // GetMatrixHook returns Matrix metadata 73 - func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta { 76 + // Metadata returns Matrix metadata 77 + func (matrixHandler) Metadata(w *webhook_model.Webhook) any { 74 78 s := &MatrixMeta{} 75 79 if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 76 - log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err) 80 + log.Error("matrixHandler.Metadata(%d): %v", w.ID, err) 77 81 } 78 82 return s 79 83 }
+1 -1
services/webhook/matrix_test.go
··· 211 211 PayloadVersion: 2, 212 212 } 213 213 214 - req, reqBody, err := newMatrixRequest(context.Background(), hook, task) 214 + req, reqBody, err := matrixHandler{}.NewRequest(context.Background(), hook, task) 215 215 require.NotNil(t, req) 216 216 require.NotNil(t, reqBody) 217 217 require.NoError(t, err)
+6 -1
services/webhook/msteams.go
··· 17 17 webhook_module "code.gitea.io/gitea/modules/webhook" 18 18 ) 19 19 20 + type msteamsHandler struct{} 21 + 22 + func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } 23 + func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } 24 + 20 25 type ( 21 26 // MSTeamsFact for Fact Structure 22 27 MSTeamsFact struct { ··· 347 352 348 353 var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} 349 354 350 - func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 355 + func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 351 356 return newJSONRequest(msteamsConvertor{}, w, t, true) 352 357 }
+1 -1
services/webhook/msteams_test.go
··· 439 439 PayloadVersion: 2, 440 440 } 441 441 442 - req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task) 442 + req, reqBody, err := msteamsHandler{}.NewRequest(context.Background(), hook, task) 443 443 require.NotNil(t, req) 444 444 require.NotNil(t, reqBody) 445 445 require.NoError(t, err)
+10 -5
services/webhook/packagist.go
··· 11 11 webhook_model "code.gitea.io/gitea/models/webhook" 12 12 "code.gitea.io/gitea/modules/json" 13 13 "code.gitea.io/gitea/modules/log" 14 + webhook_module "code.gitea.io/gitea/modules/webhook" 14 15 ) 16 + 17 + type packagistHandler struct{} 18 + 19 + func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } 15 20 16 21 type ( 17 22 // PackagistPayload represents a packagist payload ··· 30 35 } 31 36 ) 32 37 33 - // GetPackagistHook returns packagist metadata 34 - func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta { 38 + // Metadata returns packagist metadata 39 + func (packagistHandler) Metadata(w *webhook_model.Webhook) any { 35 40 s := &PackagistMeta{} 36 41 if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 37 - log.Error("webhook.GetPackagistHook(%d): %v", w.ID, err) 42 + log.Error("packagistHandler.Metadata(%d): %v", w.ID, err) 38 43 } 39 44 return s 40 45 } 41 46 42 47 // newPackagistRequest creates a request with the [PackagistPayload] for packagist (same payload for all events). 43 - func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 48 + func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 44 49 meta := &PackagistMeta{} 45 50 if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { 46 - return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err) 51 + return nil, nil, fmt.Errorf("packagistHandler.NewRequest meta json: %w", err) 47 52 } 48 53 49 54 payload := PackagistPayload{
+1 -1
services/webhook/packagist_test.go
··· 53 53 PayloadVersion: 2, 54 54 } 55 55 56 - req, reqBody, err := newPackagistRequest(context.Background(), hook, task) 56 + req, reqBody, err := packagistHandler{}.NewRequest(context.Background(), hook, task) 57 57 require.NotNil(t, req) 58 58 require.NotNil(t, reqBody) 59 59 require.NoError(t, err)
+9 -5
services/webhook/slack.go
··· 19 19 webhook_module "code.gitea.io/gitea/modules/webhook" 20 20 ) 21 21 22 + type slackHandler struct{} 23 + 24 + func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } 25 + 22 26 // SlackMeta contains the slack metadata 23 27 type SlackMeta struct { 24 28 Channel string `json:"channel"` ··· 27 31 Color string `json:"color"` 28 32 } 29 33 30 - // GetSlackHook returns slack metadata 31 - func GetSlackHook(w *webhook_model.Webhook) *SlackMeta { 34 + // Metadata returns slack metadata 35 + func (slackHandler) Metadata(w *webhook_model.Webhook) any { 32 36 s := &SlackMeta{} 33 37 if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 34 - log.Error("webhook.GetSlackHook(%d): %v", w.ID, err) 38 + log.Error("slackHandler.Metadata(%d): %v", w.ID, err) 35 39 } 36 40 return s 37 41 } ··· 283 287 284 288 var _ payloadConvertor[SlackPayload] = slackConvertor{} 285 289 286 - func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 290 + func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 287 291 meta := &SlackMeta{} 288 292 if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { 289 - return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err) 293 + return nil, nil, fmt.Errorf("slackHandler.NewRequest meta json: %w", err) 290 294 } 291 295 sc := slackConvertor{ 292 296 Channel: meta.Channel,
+52 -1
services/webhook/slack_test.go
··· 178 178 PayloadVersion: 2, 179 179 } 180 180 181 - req, reqBody, err := newSlackRequest(context.Background(), hook, task) 181 + req, reqBody, err := slackHandler{}.NewRequest(context.Background(), hook, task) 182 182 require.NotNil(t, req) 183 183 require.NotNil(t, reqBody) 184 184 require.NoError(t, err) ··· 211 211 assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName)) 212 212 } 213 213 } 214 + 215 + func TestSlackMetadata(t *testing.T) { 216 + w := &webhook_model.Webhook{ 217 + Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, 218 + } 219 + slackHook := slackHandler{}.Metadata(w) 220 + assert.Equal(t, *slackHook.(*SlackMeta), SlackMeta{ 221 + Channel: "foo", 222 + Username: "username", 223 + Color: "blue", 224 + }) 225 + } 226 + 227 + func TestSlackToHook(t *testing.T) { 228 + w := &webhook_model.Webhook{ 229 + Type: webhook_module.SLACK, 230 + ContentType: webhook_model.ContentTypeJSON, 231 + URL: "https://slack.example.com", 232 + Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, 233 + HookEvent: &webhook_module.HookEvent{ 234 + PushOnly: true, 235 + SendEverything: false, 236 + ChooseEvents: false, 237 + HookEvents: webhook_module.HookEvents{ 238 + Create: false, 239 + Push: true, 240 + PullRequest: false, 241 + }, 242 + }, 243 + } 244 + h, err := ToHook("repoLink", w) 245 + assert.NoError(t, err) 246 + 247 + assert.Equal(t, h.Config, map[string]string{ 248 + "url": "https://slack.example.com", 249 + "content_type": "json", 250 + 251 + "channel": "foo", 252 + "color": "blue", 253 + "icon_url": "", 254 + "username": "username", 255 + }) 256 + assert.Equal(t, h.URL, "https://slack.example.com") 257 + assert.Equal(t, h.ContentType, "json") 258 + assert.Equal(t, h.Metadata, &SlackMeta{ 259 + Channel: "foo", 260 + Username: "username", 261 + IconURL: "", 262 + Color: "blue", 263 + }) 264 + }
+8 -4
services/webhook/telegram.go
··· 17 17 webhook_module "code.gitea.io/gitea/modules/webhook" 18 18 ) 19 19 20 + type telegramHandler struct{} 21 + 22 + func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } 23 + 20 24 type ( 21 25 // TelegramPayload represents 22 26 TelegramPayload struct { ··· 33 37 } 34 38 ) 35 39 36 - // GetTelegramHook returns telegram metadata 37 - func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta { 40 + // Metadata returns telegram metadata 41 + func (telegramHandler) Metadata(w *webhook_model.Webhook) any { 38 42 s := &TelegramMeta{} 39 43 if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 40 - log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err) 44 + log.Error("telegramHandler.Metadata(%d): %v", w.ID, err) 41 45 } 42 46 return s 43 47 } ··· 191 195 192 196 var _ payloadConvertor[TelegramPayload] = telegramConvertor{} 193 197 194 - func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 198 + func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 195 199 return newJSONRequest(telegramConvertor{}, w, t, true) 196 200 }
+1 -1
services/webhook/telegram_test.go
··· 186 186 PayloadVersion: 2, 187 187 } 188 188 189 - req, reqBody, err := newTelegramRequest(context.Background(), hook, task) 189 + req, reqBody, err := telegramHandler{}.NewRequest(context.Background(), hook, task) 190 190 require.NotNil(t, req) 191 191 require.NotNil(t, reqBody) 192 192 require.NoError(t, err)
+36 -15
services/webhook/webhook.go
··· 27 27 "github.com/gobwas/glob" 28 28 ) 29 29 30 - var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ 31 - webhook_module.SLACK: newSlackRequest, 32 - webhook_module.DISCORD: newDiscordRequest, 33 - webhook_module.DINGTALK: newDingtalkRequest, 34 - webhook_module.TELEGRAM: newTelegramRequest, 35 - webhook_module.MSTEAMS: newMSTeamsRequest, 36 - webhook_module.FEISHU: newFeishuRequest, 37 - webhook_module.MATRIX: newMatrixRequest, 38 - webhook_module.WECHATWORK: newWechatworkRequest, 39 - webhook_module.PACKAGIST: newPackagistRequest, 30 + type Handler interface { 31 + Type() webhook_module.HookType 32 + NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) 33 + Metadata(*webhook_model.Webhook) any 34 + } 35 + 36 + var webhookHandlers = []Handler{ 37 + defaultHandler{true}, 38 + defaultHandler{false}, 39 + gogsHandler{}, 40 + 41 + slackHandler{}, 42 + discordHandler{}, 43 + dingtalkHandler{}, 44 + telegramHandler{}, 45 + msteamsHandler{}, 46 + feishuHandler{}, 47 + matrixHandler{}, 48 + wechatworkHandler{}, 49 + packagistHandler{}, 50 + } 51 + 52 + // GetWebhookHandler return the handler for a given webhook type (nil if not found) 53 + func GetWebhookHandler(name webhook_module.HookType) Handler { 54 + for _, h := range webhookHandlers { 55 + if h.Type() == name { 56 + return h 57 + } 58 + } 59 + return nil 60 + } 61 + 62 + // List provides a list of the supported webhooks 63 + func List() []Handler { 64 + return webhookHandlers 40 65 } 41 66 42 67 // IsValidHookTaskType returns true if a webhook registered 43 68 func IsValidHookTaskType(name string) bool { 44 - if name == webhook_module.FORGEJO || name == webhook_module.GITEA || name == webhook_module.GOGS { 45 - return true 46 - } 47 - _, ok := webhookRequesters[name] 48 - return ok 69 + return GetWebhookHandler(name) != nil 49 70 } 50 71 51 72 // hookQueue is a global queue of web hooks
-12
services/webhook/webhook_test.go
··· 16 16 "github.com/stretchr/testify/assert" 17 17 ) 18 18 19 - func TestWebhook_GetSlackHook(t *testing.T) { 20 - w := &webhook_model.Webhook{ 21 - Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, 22 - } 23 - slackHook := GetSlackHook(w) 24 - assert.Equal(t, *slackHook, SlackMeta{ 25 - Channel: "foo", 26 - Username: "username", 27 - Color: "blue", 28 - }) 29 - } 30 - 31 19 func activateWebhook(t *testing.T, hookID int64) { 32 20 t.Helper() 33 21 updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(webhook_model.Webhook{IsActive: true})
+6 -1
services/webhook/wechatwork.go
··· 15 15 webhook_module "code.gitea.io/gitea/modules/webhook" 16 16 ) 17 17 18 + type wechatworkHandler struct{} 19 + 20 + func (wechatworkHandler) Type() webhook_module.HookType { return webhook_module.WECHATWORK } 21 + func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } 22 + 18 23 type ( 19 24 // WechatworkPayload represents 20 25 WechatworkPayload struct { ··· 177 182 178 183 var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} 179 184 180 - func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 185 + func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 181 186 return newJSONRequest(wechatworkConvertor{}, w, t, true) 182 187 }
+2 -2
templates/repo/settings/webhook/discord.tmpl
··· 8 8 </div> 9 9 <div class="field"> 10 10 <label for="username">{{ctx.Locale.Tr "repo.settings.discord_username"}}</label> 11 - <input id="username" name="username" value="{{.DiscordHook.Username}}" placeholder="Forgejo"> 11 + <input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo"> 12 12 </div> 13 13 <div class="field"> 14 14 <label for="icon_url">{{ctx.Locale.Tr "repo.settings.discord_icon_url"}}</label> 15 - <input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="https://example.com/assets/img/logo.svg"> 15 + <input id="icon_url" name="icon_url" value="{{.HookMetadata.IconURL}}" placeholder="https://example.com/assets/img/logo.svg"> 16 16 </div> 17 17 {{template "repo/settings/webhook/settings" .}} 18 18 </form>
+3 -3
templates/repo/settings/webhook/matrix.tmpl
··· 4 4 {{.CsrfTokenHtml}} 5 5 <div class="required field {{if .Err_HomeserverURL}}error{{end}}"> 6 6 <label for="homeserver_url">{{ctx.Locale.Tr "repo.settings.matrix.homeserver_url"}}</label> 7 - <input id="homeserver_url" name="homeserver_url" type="url" value="{{.MatrixHook.HomeserverURL}}" autofocus required> 7 + <input id="homeserver_url" name="homeserver_url" type="url" value="{{.HookMetadata.HomeserverURL}}" autofocus required> 8 8 </div> 9 9 <div class="required field {{if .Err_Room}}error{{end}}"> 10 10 <label for="room_id">{{ctx.Locale.Tr "repo.settings.matrix.room_id"}}</label> 11 - <input id="room_id" name="room_id" type="text" value="{{.MatrixHook.Room}}" required> 11 + <input id="room_id" name="room_id" type="text" value="{{.HookMetadata.Room}}" required> 12 12 </div> 13 13 <div class="field"> 14 14 <label>{{ctx.Locale.Tr "repo.settings.matrix.message_type"}}</label> 15 15 <div class="ui selection dropdown"> 16 - <input type="hidden" id="message_type" name="message_type" value="{{if .MatrixHook.MessageType}}{{.MatrixHook.MessageType}}{{else}}1{{end}}"> 16 + <input type="hidden" id="message_type" name="message_type" value="{{if .HookMetadata.MessageType}}{{.HookMetadata.MessageType}}{{else}}1{{end}}"> 17 17 <div class="default text"></div> 18 18 {{svg "octicon-triangle-down" 14 "dropdown icon"}} 19 19 <div class="menu">
+3 -3
templates/repo/settings/webhook/packagist.tmpl
··· 4 4 {{.CsrfTokenHtml}} 5 5 <div class="required field {{if .Err_Username}}error{{end}}"> 6 6 <label for="username">{{ctx.Locale.Tr "repo.settings.packagist_username"}}</label> 7 - <input id="username" name="username" value="{{.PackagistHook.Username}}" placeholder="Forgejo" autofocus required> 7 + <input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo" autofocus required> 8 8 </div> 9 9 <div class="required field {{if .Err_APIToken}}error{{end}}"> 10 10 <label for="api_token">{{ctx.Locale.Tr "repo.settings.packagist_api_token"}}</label> 11 - <input id="api_token" name="api_token" value="{{.PackagistHook.APIToken}}" placeholder="X5F_tZ-Wj3c1vqaU2Rky" required> 11 + <input id="api_token" name="api_token" value="{{.HookMetadata.APIToken}}" placeholder="X5F_tZ-Wj3c1vqaU2Rky" required> 12 12 </div> 13 13 <div class="required field {{if .Err_PackageURL}}error{{end}}"> 14 14 <label for="package_url">{{ctx.Locale.Tr "repo.settings.packagist_package_url"}}</label> 15 - <input id="package_url" name="package_url" value="{{.PackagistHook.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required> 15 + <input id="package_url" name="package_url" value="{{.HookMetadata.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required> 16 16 </div> 17 17 {{template "repo/settings/webhook/settings" .}} 18 18 </form>
+4 -4
templates/repo/settings/webhook/slack.tmpl
··· 8 8 </div> 9 9 <div class="required field {{if .Err_Channel}}error{{end}}"> 10 10 <label for="channel">{{ctx.Locale.Tr "repo.settings.slack_channel"}}</label> 11 - <input id="channel" name="channel" value="{{.SlackHook.Channel}}" placeholder="#general" required> 11 + <input id="channel" name="channel" value="{{.HookMetadata.Channel}}" placeholder="#general" required> 12 12 </div> 13 13 14 14 <div class="field"> 15 15 <label for="username">{{ctx.Locale.Tr "repo.settings.slack_username"}}</label> 16 - <input id="username" name="username" value="{{.SlackHook.Username}}" placeholder="Forgejo"> 16 + <input id="username" name="username" value="{{.HookMetadata.Username}}" placeholder="Forgejo"> 17 17 </div> 18 18 <div class="field"> 19 19 <label for="icon_url">{{ctx.Locale.Tr "repo.settings.slack_icon_url"}}</label> 20 - <input id="icon_url" name="icon_url" value="{{.SlackHook.IconURL}}" placeholder="https://example.com/img/favicon.png"> 20 + <input id="icon_url" name="icon_url" value="{{.HookMetadata.IconURL}}" placeholder="https://example.com/img/favicon.png"> 21 21 </div> 22 22 <div class="field"> 23 23 <label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label> 24 - <input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="#dd4b39, good, warning, danger"> 24 + <input id="color" name="color" value="{{.HookMetadata.Color}}" placeholder="#dd4b39, good, warning, danger"> 25 25 </div> 26 26 {{template "repo/settings/webhook/settings" .}} 27 27 </form>
+3 -3
templates/repo/settings/webhook/telegram.tmpl
··· 4 4 {{.CsrfTokenHtml}} 5 5 <div class="required field {{if .Err_BotToken}}error{{end}}"> 6 6 <label for="bot_token">{{ctx.Locale.Tr "repo.settings.bot_token"}}</label> 7 - <input id="bot_token" name="bot_token" type="text" value="{{.TelegramHook.BotToken}}" autofocus required> 7 + <input id="bot_token" name="bot_token" type="text" value="{{.HookMetadata.BotToken}}" autofocus required> 8 8 </div> 9 9 <div class="required field {{if .Err_ChatID}}error{{end}}"> 10 10 <label for="chat_id">{{ctx.Locale.Tr "repo.settings.chat_id"}}</label> 11 - <input id="chat_id" name="chat_id" type="text" value="{{.TelegramHook.ChatID}}" required> 11 + <input id="chat_id" name="chat_id" type="text" value="{{.HookMetadata.ChatID}}" required> 12 12 </div> 13 13 <div class="field {{if .Err_ThreadID}}error{{end}}"> 14 14 <label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label> 15 - <input id="thread_id" name="thread_id" type="text" value="{{.TelegramHook.ThreadID}}"> 15 + <input id="thread_id" name="thread_id" type="text" value="{{.HookMetadata.ThreadID}}"> 16 16 </div> 17 17 {{template "repo/settings/webhook/settings" .}} 18 18 </form>
+12
templates/swagger/v1_json.tmpl
··· 20952 20952 "x-go-name": "BranchFilter" 20953 20953 }, 20954 20954 "config": { 20955 + "description": "Deprecated: use Metadata instead", 20955 20956 "type": "object", 20956 20957 "additionalProperties": { 20957 20958 "type": "string" 20958 20959 }, 20959 20960 "x-go-name": "Config" 20961 + }, 20962 + "content_type": { 20963 + "type": "string", 20964 + "x-go-name": "ContentType" 20960 20965 }, 20961 20966 "created_at": { 20962 20967 "type": "string", ··· 20975 20980 "format": "int64", 20976 20981 "x-go-name": "ID" 20977 20982 }, 20983 + "metadata": { 20984 + "x-go-name": "Metadata" 20985 + }, 20978 20986 "type": { 20979 20987 "type": "string", 20980 20988 "x-go-name": "Type" ··· 20983 20991 "type": "string", 20984 20992 "format": "date-time", 20985 20993 "x-go-name": "Updated" 20994 + }, 20995 + "url": { 20996 + "type": "string", 20997 + "x-go-name": "URL" 20986 20998 } 20987 20999 }, 20988 21000 "x-go-package": "code.gitea.io/gitea/modules/structs"
+1
tests/integration/api_repo_hook_test.go
··· 40 40 var apiHook *api.Hook 41 41 DecodeJSON(t, resp, &apiHook) 42 42 assert.Equal(t, "http://example.com/", apiHook.Config["url"]) 43 + assert.Equal(t, "http://example.com/", apiHook.URL) 43 44 assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader) 44 45 }
+2 -1
tests/integration/repo_webhook_test.go
··· 11 11 "testing" 12 12 13 13 gitea_context "code.gitea.io/gitea/services/context" 14 + "code.gitea.io/gitea/services/webhook" 14 15 "code.gitea.io/gitea/tests" 15 16 16 17 "github.com/PuerkitoBio/goquery" ··· 21 22 defer tests.PrepareTestEnv(t)() 22 23 session := loginUser(t, "user2") 23 24 24 - webhooksLen := 12 25 + webhooksLen := len(webhook.List()) 25 26 baseurl := "/user2/repo1/settings/hooks" 26 27 tests := []string{ 27 28 // webhook list page