aboutsummaryrefslogtreecommitdiffstats
path: root/services/webhook
diff options
context:
space:
mode:
Diffstat (limited to 'services/webhook')
-rw-r--r--services/webhook/deliver.go34
-rw-r--r--services/webhook/deliver_test.go19
-rw-r--r--services/webhook/dingtalk.go28
-rw-r--r--services/webhook/dingtalk_test.go3
-rw-r--r--services/webhook/discord.go33
-rw-r--r--services/webhook/discord_test.go3
-rw-r--r--services/webhook/feishu.go58
-rw-r--r--services/webhook/feishu_test.go9
-rw-r--r--services/webhook/general.go132
-rw-r--r--services/webhook/general_test.go4
-rw-r--r--services/webhook/matrix.go26
-rw-r--r--services/webhook/matrix_test.go3
-rw-r--r--services/webhook/msteams.go61
-rw-r--r--services/webhook/msteams_test.go7
-rw-r--r--services/webhook/notifier.go152
-rw-r--r--services/webhook/packagist.go16
-rw-r--r--services/webhook/packagist_test.go7
-rw-r--r--services/webhook/payloader.go14
-rw-r--r--services/webhook/slack.go28
-rw-r--r--services/webhook/slack_test.go3
-rw-r--r--services/webhook/telegram.go24
-rw-r--r--services/webhook/telegram_test.go3
-rw-r--r--services/webhook/webhook.go26
-rw-r--r--services/webhook/webhook_test.go14
-rw-r--r--services/webhook/wechatwork.go22
25 files changed, 608 insertions, 121 deletions
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 4707602cdf..e8e6ed19c1 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -10,6 +10,7 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
+ "errors"
"fmt"
"io"
"net/http"
@@ -18,6 +19,7 @@ import (
"sync"
"time"
+ user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/hostmatcher"
@@ -40,7 +42,7 @@ func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook
case http.MethodPost:
switch w.ContentType {
case webhook_model.ContentTypeJSON:
- req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
+ req, err = http.NewRequest(http.MethodPost, w.URL, strings.NewReader(t.PayloadContent))
if err != nil {
return nil, nil, err
}
@@ -51,7 +53,7 @@ func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook
"payload": []string{t.PayloadContent},
}
- req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
+ req, err = http.NewRequest(http.MethodPost, w.URL, strings.NewReader(forms.Encode()))
if err != nil {
return nil, nil, err
}
@@ -68,7 +70,7 @@ func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook
vals := u.Query()
vals["payload"] = []string{t.PayloadContent}
u.RawQuery = vals.Encode()
- req, err = http.NewRequest("GET", u.String(), nil)
+ req, err = http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, nil, err
}
@@ -80,7 +82,7 @@ func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook
return nil, nil, err
}
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
- req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
+ req, err = http.NewRequest(http.MethodPut, url, strings.NewReader(t.PayloadContent))
if err != nil {
return nil, nil, err
}
@@ -92,10 +94,10 @@ func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook
}
body = []byte(t.PayloadContent)
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), w, t, body)
}
-func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
+func addDefaultHeaders(req *http.Request, secret []byte, w *webhook_model.Webhook, t *webhook_model.HookTask, payloadContent []byte) error {
var signatureSHA1 string
var signatureSHA256 string
if len(secret) > 0 {
@@ -112,10 +114,27 @@ func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTa
event := t.EventType.Event()
eventType := string(t.EventType)
+ targetType := "default"
+ if w.IsSystemWebhook {
+ targetType = "system"
+ } else if w.RepoID != 0 {
+ targetType = "repository"
+ } else if w.OwnerID != 0 {
+ owner, err := user_model.GetUserByID(req.Context(), w.OwnerID)
+ if owner != nil && err == nil {
+ if owner.IsOrganization() {
+ targetType = "organization"
+ } else {
+ targetType = "user"
+ }
+ }
+ }
+
req.Header.Add("X-Gitea-Delivery", t.UUID)
req.Header.Add("X-Gitea-Event", event)
req.Header.Add("X-Gitea-Event-Type", eventType)
req.Header.Add("X-Gitea-Signature", signatureSHA256)
+ req.Header.Add("X-Gitea-Hook-Installation-Target-Type", targetType)
req.Header.Add("X-Gogs-Delivery", t.UUID)
req.Header.Add("X-Gogs-Event", event)
req.Header.Add("X-Gogs-Event-Type", eventType)
@@ -125,6 +144,7 @@ func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTa
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
+ req.Header["X-GitHub-Hook-Installation-Target-Type"] = []string{targetType}
return nil
}
@@ -309,7 +329,7 @@ func Init() error {
hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler)
if hookQueue == nil {
- return fmt.Errorf("unable to create webhook_sender queue")
+ return errors.New("unable to create webhook_sender queue")
}
go graceful.GetManager().RunWithCancel(hookQueue)
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index d0cfc1598f..1d32d7b772 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"io"
"net/http"
"net/http/httptest"
@@ -65,7 +64,7 @@ func TestWebhookProxy(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.req, func(t *testing.T) {
- req, err := http.NewRequest("POST", tt.req, nil)
+ req, err := http.NewRequest(http.MethodPost, tt.req, nil)
require.NoError(t, err)
u, err := webhookProxy(allowedHostMatcher)(req)
@@ -92,7 +91,7 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/webhook", r.URL.Path)
assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
done <- struct{}{}
}))
t.Cleanup(s.Close)
@@ -118,7 +117,7 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, hookTask)
- assert.NoError(t, Deliver(context.Background(), hookTask))
+ assert.NoError(t, Deliver(t.Context(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
@@ -139,7 +138,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
// Version 1
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
- assert.Equal(t, "", r.Header.Get("Content-Type"))
+ assert.Empty(t, r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, `{"data": 42}`, string(body))
@@ -153,11 +152,11 @@ func TestWebhookDeliverHookTask(t *testing.T) {
assert.Len(t, body, 2147)
default:
- w.WriteHeader(404)
+ w.WriteHeader(http.StatusNotFound)
t.Fatalf("unexpected url path %s", r.URL.Path)
return
}
- w.WriteHeader(200)
+ w.WriteHeader(http.StatusOK)
done <- struct{}{}
}))
t.Cleanup(s.Close)
@@ -185,7 +184,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, hookTask)
- assert.NoError(t, Deliver(context.Background(), hookTask))
+ assert.NoError(t, Deliver(t.Context(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
@@ -211,7 +210,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, hookTask)
- assert.NoError(t, Deliver(context.Background(), hookTask))
+ assert.NoError(t, Deliver(t.Context(), hookTask))
select {
case <-done:
case <-time.After(5 * time.Second):
@@ -280,7 +279,7 @@ func TestWebhookDeliverSpecificTypes(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, hookTask)
- assert.NoError(t, Deliver(context.Background(), hookTask))
+ assert.NoError(t, Deliver(t.Context(), hookTask))
select {
case gotBody := <-cases[typ].gotBody:
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index e382f5a9df..5bbc610fe5 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -30,7 +30,7 @@ func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
- return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
+ return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
}
// Delete implements PayloadConvertor Delete method
@@ -39,14 +39,14 @@ func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
- return createDingtalkPayload(title, title, fmt.Sprintf("view ref %s", refName), p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
+ return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
}
// Fork implements PayloadConvertor Fork method
func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
- return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil
+ return createDingtalkPayload(title, title, "view forked repo "+p.Repo.FullName, p.Repo.HTMLURL), nil
}
// Push implements PayloadConvertor Push method
@@ -170,6 +170,24 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}
+func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
+}
+
+func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
+}
+
+func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+ return createDingtalkPayload(text, text, "Workflow Job", p.WorkflowJob.HTMLURL), nil
+}
+
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
return DingtalkPayload{
MsgType: "actionCard",
@@ -190,3 +208,7 @@ func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest)
+}
diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go
index 25f47347d0..763d23048a 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"net/url"
"testing"
@@ -236,7 +235,7 @@ func TestDingTalkJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newDingtalkRequest(context.Background(), hook, task)
+ req, reqBody, err := newDingtalkRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index c562d98168..0426964181 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -101,6 +101,13 @@ var (
redColor = color("ff3232")
)
+// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
+// Discord has some limits in place for the embeds.
+// According to some tests, there is no consistent limit for different character sets.
+// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
+// To keep it simple, we currently truncate at 2000.
+const discordDescriptionCharactersLimit = 2000
+
type discordConvertor struct {
Username string
AvatarURL string
@@ -265,6 +272,24 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
+func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
+ text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+ return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
+}
+
+func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
+ text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+ return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
+}
+
+func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
+ text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
+
+ return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
+}
+
func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &DiscordMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
@@ -277,12 +302,16 @@ func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
return newJSONRequest(pc, w, t, true)
}
+func init() {
+ RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest)
+}
+
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
switch event {
case webhook_module.HookEventPullRequestReviewApproved:
return "approved", nil
case webhook_module.HookEventPullRequestReviewRejected:
- return "rejected", nil
+ return "requested changes", nil
case webhook_module.HookEventPullRequestReviewComment:
return "comment", nil
default:
@@ -297,7 +326,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
Embeds: []DiscordEmbed{
{
Title: title,
- Description: text,
+ Description: util.TruncateRunes(text, discordDescriptionCharactersLimit),
URL: url,
Color: color,
Author: DiscordEmbedAuthor{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index 36b99d452e..7f503e3374 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -303,7 +302,7 @@ func TestDiscordJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newDiscordRequest(context.Background(), hook, task)
+ req, reqBody, err := newDiscordRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 7ca7d1cf5f..b6ee80c44c 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -5,9 +5,13 @@ package webhook
import (
"context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
"fmt"
"net/http"
"strings"
+ "time"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
@@ -16,10 +20,12 @@ import (
)
type (
- // FeishuPayload represents
+ // FeishuPayload represents the payload for Feishu webhook
FeishuPayload struct {
- MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
- Content struct {
+ Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
+ Sign string `json:"sign,omitempty"` // Signature for verification
+ MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
+ Content struct {
Text string `json:"text"`
} `json:"content"`
}
@@ -166,7 +172,49 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
return newFeishuTextPayload(text), nil
}
+func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
+func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
+func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
+// feishuGenSign generates a signature for Feishu webhook
+// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
+func feishuGenSign(secret string, timestamp int64) string {
+ // key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
+ stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
+ h := hmac.New(sha256.New, []byte(stringToSign))
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
+
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
- var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
- return newJSONRequest(pc, w, t, true)
+ payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Add timestamp and signature if secret is provided
+ if w.Secret != "" {
+ timestamp := time.Now().Unix()
+ payload.Timestamp = timestamp
+ payload.Sign = feishuGenSign(w.Secret, timestamp)
+ }
+
+ return prepareJSONRequest(payload, w, t, false /* no default headers */)
+}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest)
}
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index ef18333fd4..7e200ea132 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -169,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
URL: "https://feishu.example.com/",
Meta: `{}`,
HTTPMethod: "POST",
+ Secret: "secret",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
@@ -177,17 +177,20 @@ func TestFeishuJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
+ req, reqBody, err := newFeishuRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
- assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body FeishuPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
+ assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
+
+ // a separate sign test, the result is generated by official python code, so the algo must be correct
+ assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
}
diff --git a/services/webhook/general.go b/services/webhook/general.go
index dde43bb349..be457e46f5 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -9,7 +9,9 @@ import (
"net/url"
"strings"
+ user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -37,19 +39,20 @@ func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, o
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- if p.Action == api.HookIssueAssigned {
+ switch p.Action {
+ case api.HookIssueAssigned:
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- } else if p.Action == api.HookIssueUnassigned {
- operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- } else if p.Action == api.HookIssueMilestoned {
+ case api.HookIssueUnassigned:
+ operateResult = p.Sender.UserName + " unassigned this for someone"
+ case api.HookIssueMilestoned:
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
}
link = p.PullRequest.HTMLURL
- by = fmt.Sprintf("PullRequest by %s", p.PullRequest.Poster.UserName)
+ by = "PullRequest by " + p.PullRequest.Poster.UserName
if len(assignStringList) > 0 {
- assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
+ assignees = "Assignees: " + strings.Join(assignStringList, ", ")
}
- operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
+ operator = "Operator: " + p.Sender.UserName
return title, link, by, operator, operateResult, assignees
}
@@ -62,19 +65,20 @@ func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operate
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- if p.Action == api.HookIssueAssigned {
+ switch p.Action {
+ case api.HookIssueAssigned:
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- } else if p.Action == api.HookIssueUnassigned {
- operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- } else if p.Action == api.HookIssueMilestoned {
+ case api.HookIssueUnassigned:
+ operateResult = p.Sender.UserName + " unassigned this for someone"
+ case api.HookIssueMilestoned:
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
}
link = p.Issue.HTMLURL
- by = fmt.Sprintf("Issue by %s", p.Issue.Poster.UserName)
+ by = "Issue by " + p.Issue.Poster.UserName
if len(assignStringList) > 0 {
- assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
+ assignees = "Assignees: " + strings.Join(assignStringList, ", ")
}
- operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
+ operator = "Operator: " + p.Sender.UserName
return issueTitle, link, by, operator, operateResult, assignees
}
@@ -83,11 +87,11 @@ func getIssuesCommentInfo(p *api.IssueCommentPayload) (title, link, by, operator
title = fmt.Sprintf("[Comment-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
link = p.Issue.HTMLURL
if p.IsPull {
- by = fmt.Sprintf("PullRequest by %s", p.Issue.Poster.UserName)
+ by = "PullRequest by " + p.Issue.Poster.UserName
} else {
- by = fmt.Sprintf("Issue by %s", p.Issue.Poster.UserName)
+ by = "Issue by " + p.Issue.Poster.UserName
}
- operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
+ operator = "Operator: " + p.Sender.UserName
return title, link, by, operator
}
@@ -131,7 +135,7 @@ func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, with
text = fmt.Sprintf("[%s] Issue milestone cleared: %s", repoLink, titleLink)
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
}
if p.Action == api.HookIssueOpened || p.Action == api.HookIssueEdited {
@@ -196,7 +200,7 @@ func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkForm
text = fmt.Sprintf("[%s] Pull request review request removed: %s", repoLink, titleLink)
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
}
return text, issueTitle, extraMarkdown, color
@@ -218,7 +222,7 @@ func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, w
color = redColor
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
}
return text, color
@@ -247,7 +251,7 @@ func getWikiPayloadInfo(p *api.WikiPayload, linkFormatter linkFormatter, withSen
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
}
return text, color, pageLink
@@ -283,7 +287,7 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
color = redColor
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
}
return text, issueTitle, color
@@ -294,14 +298,92 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
switch p.Action {
case api.HookPackageCreated:
- text = fmt.Sprintf("Package created: %s", refLink)
+ text = "Package created: " + refLink
color = greenColor
case api.HookPackageDeleted:
- text = fmt.Sprintf("Package deleted: %s", refLink)
+ text = "Package deleted: " + refLink
color = redColor
}
if withSender {
- text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+ }
+
+ return text, color
+}
+
+func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+ refLink := linkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
+
+ text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
+ color = greenColor
+ if withSender {
+ if user_model.IsGiteaActionsUserName(p.Sender.UserName) {
+ text += " by " + p.Sender.FullName
+ } else {
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+ }
+ }
+
+ return text, color
+}
+
+func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+ description := p.WorkflowRun.Conclusion
+ if description == "" {
+ description = p.WorkflowRun.Status
+ }
+ refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description)
+
+ text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
+ switch description {
+ case "waiting":
+ color = orangeColor
+ case "queued":
+ color = orangeColorLight
+ case "success":
+ color = greenColor
+ case "failure":
+ color = redColor
+ case "cancelled":
+ color = yellowColor
+ case "skipped":
+ color = purpleColor
+ default:
+ color = greyColor
+ }
+ if withSender {
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+ }
+
+ return text, color
+}
+
+func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+ description := p.WorkflowJob.Conclusion
+ if description == "" {
+ description = p.WorkflowJob.Status
+ }
+ refLink := linkFormatter(p.WorkflowJob.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowJob.Name, p.WorkflowJob.RunID)+"["+base.ShortSha(p.WorkflowJob.HeadSha)+"]:"+description)
+
+ text = fmt.Sprintf("Workflow Job %s: %s", p.Action, refLink)
+ switch description {
+ case "waiting":
+ color = orangeColor
+ case "queued":
+ color = orangeColorLight
+ case "success":
+ color = greenColor
+ case "failure":
+ color = redColor
+ case "cancelled":
+ color = yellowColor
+ case "skipped":
+ color = purpleColor
+ default:
+ color = greyColor
+ }
+ if withSender {
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
}
return text, color
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index ef1ec7f324..ec735d785a 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -319,8 +319,8 @@ func packageTestPayload() *api.PackagePayload {
AvatarURL: "http://localhost:3000/user1/avatar",
},
Repository: nil,
- Organization: &api.User{
- UserName: "org1",
+ Organization: &api.Organization{
+ Name: "org1",
AvatarURL: "http://localhost:3000/org1/avatar",
},
Package: &api.Package{
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 5e9f808d8b..3e9163f78c 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -15,6 +15,7 @@ import (
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
@@ -24,6 +25,10 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
)
+func init() {
+ RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest)
+}
+
func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &MatrixMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
@@ -52,7 +57,7 @@ func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mo
}
req.Header.Set("Content-Type", "application/json")
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), w, t, body) // likely useless, but has always been sent historially
}
const matrixPayloadSizeLimit = 1024 * 64
@@ -240,6 +245,25 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
return m.newPayload(text)
}
+func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
+ refLink := htmlLinkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
+ text := fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
+
+ return m.newPayload(text)
+}
+
+func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+ return m.newPayload(text)
+}
+
+func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
+
+ return m.newPayload(text)
+}
+
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
func getMessageBody(htmlText string) string {
diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go
index 058f8e3c5f..d36d93c5a7 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -211,7 +210,7 @@ func TestMatrixJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newMatrixRequest(context.Background(), hook, task)
+ req, reqBody, err := newMatrixRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 7ef96ffa27..450a544b42 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strconv"
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -73,7 +74,7 @@ func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
"",
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
greenColor,
- &MSTeamsFact{fmt.Sprintf("%s:", p.RefType), refName},
+ &MSTeamsFact{p.RefType + ":", refName},
), nil
}
@@ -90,7 +91,7 @@ func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
"",
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
yellowColor,
- &MSTeamsFact{fmt.Sprintf("%s:", p.RefType), refName},
+ &MSTeamsFact{p.RefType + ":", refName},
), nil
}
@@ -148,7 +149,7 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
text,
titleLink,
greenColor,
- &MSTeamsFact{"Commit count:", fmt.Sprintf("%d", p.TotalCommits)},
+ &MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)},
), nil
}
@@ -163,7 +164,7 @@ func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
extraMarkdown,
p.Issue.HTMLURL,
color,
- &MSTeamsFact{"Issue #:", fmt.Sprintf("%d", p.Issue.ID)},
+ &MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
), nil
}
@@ -178,7 +179,7 @@ func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPaylo
p.Comment.Body,
p.Comment.HTMLURL,
color,
- &MSTeamsFact{"Issue #:", fmt.Sprintf("%d", p.Issue.ID)},
+ &MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
), nil
}
@@ -193,7 +194,7 @@ func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload
extraMarkdown,
p.PullRequest.HTMLURL,
color,
- &MSTeamsFact{"Pull request #:", fmt.Sprintf("%d", p.PullRequest.ID)},
+ &MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)},
), nil
}
@@ -230,7 +231,7 @@ func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module
text,
p.PullRequest.HTMLURL,
color,
- &MSTeamsFact{"Pull request #:", fmt.Sprintf("%d", p.PullRequest.ID)},
+ &MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)},
), nil
}
@@ -303,6 +304,48 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
), nil
}
+func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
+ title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repo,
+ p.Sender,
+ title,
+ "",
+ p.TargetURL,
+ color,
+ &MSTeamsFact{"CommitStatus:", p.Context},
+ ), nil
+}
+
+func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
+ title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repo,
+ p.Sender,
+ title,
+ "",
+ p.WorkflowRun.HTMLURL,
+ color,
+ &MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
+ ), nil
+}
+
+func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
+ title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repo,
+ p.Sender,
+ title,
+ "",
+ p.WorkflowJob.HTMLURL,
+ color,
+ &MSTeamsFact{"WorkflowJob:", p.WorkflowJob.Name},
+ ), nil
+}
+
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2)
if r != nil {
@@ -349,3 +392,7 @@ func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest)
+}
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index 01e08b918e..0d98b94bad 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -336,7 +335,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
- assert.Equal(t, "", pl.Sections[0].Text)
+ assert.Empty(t, pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
@@ -357,7 +356,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
- assert.Equal(t, "", pl.Sections[0].Text)
+ assert.Empty(t, pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
@@ -439,7 +438,7 @@ func TestMSTeamsJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task)
+ req, reqBody, err := newMSTeamsRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index a3d5cb34b1..672abd5c95 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -6,14 +6,18 @@ package webhook
import (
"context"
+ actions_model "code.gitea.io/gitea/models/actions"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -290,6 +294,43 @@ func (m *webhookNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
}
}
+func (m *webhookNotifier) DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
+ permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if issue.IsPull {
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest: %v", err)
+ return
+ }
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
+ Action: api.HookIssueDeleted,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
+ Repository: convert.ToRepo(ctx, issue.Repo, permission),
+ Sender: convert.ToUser(ctx, doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+ } else {
+ if err := issue.LoadRepo(ctx); err != nil {
+ log.Error("issue.LoadRepo: %v", err)
+ return
+ }
+ if err := issue.LoadPoster(ctx); err != nil {
+ log.Error("issue.LoadPoster: %v", err)
+ return
+ }
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
+ Action: api.HookIssueDeleted,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue.Poster, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, permission),
+ Sender: convert.ToUser(ctx, doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+ }
+}
+
func (m *webhookNotifier) NewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) {
if err := pull.LoadIssue(ctx); err != nil {
log.Error("pull.LoadIssue: %v", err)
@@ -601,7 +642,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
apiPusher := convert.ToUser(ctx, pusher, nil)
- apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL())
+ apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo)
if err != nil {
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
return
@@ -763,12 +804,10 @@ func (m *webhookNotifier) PullRequestReviewRequest(ctx context.Context, doer *us
func (m *webhookNotifier) CreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
apiPusher := convert.ToUser(ctx, pusher, nil)
apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone})
- refName := refFullName.ShortName()
-
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventCreate, &api.CreatePayload{
- Ref: refName, // FIXME: should it be a full ref name?
+ Ref: refFullName.ShortName(), // FIXME: should it be a full ref name? But it will break the existing webhooks?
Sha: refID,
- RefType: refFullName.RefType(),
+ RefType: string(refFullName.RefType()),
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
@@ -800,11 +839,9 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use
func (m *webhookNotifier) DeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
apiPusher := convert.ToUser(ctx, pusher, nil)
apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})
- refName := refFullName.ShortName()
-
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventDelete, &api.DeletePayload{
- Ref: refName, // FIXME: should it be a full ref name?
- RefType: refFullName.RefType(),
+ Ref: refFullName.ShortName(), // FIXME: should it be a full ref name? But it will break the existing webhooks?
+ RefType: string(refFullName.RefType()),
PusherType: api.PusherTypeUser,
Repo: apiRepo,
Sender: apiPusher,
@@ -844,7 +881,7 @@ func (m *webhookNotifier) DeleteRelease(ctx context.Context, doer *user_model.Us
func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
apiPusher := convert.ToUser(ctx, pusher, nil)
- apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL())
+ apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo)
if err != nil {
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
return
@@ -868,12 +905,17 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
apiSender := convert.ToUser(ctx, sender, nil)
- apiCommit, err := repository.ToAPIPayloadCommit(ctx, map[string]*user_model.User{}, repo.RepoPath(), repo.HTMLURL(), commit)
+ apiCommit, err := repository.ToAPIPayloadCommit(ctx, map[string]*user_model.User{}, repo, commit)
if err != nil {
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
return
}
+ // as a webhook url, target should be an absolute url. But for internal actions target url
+ // the target url is a url path with no host and port to make it easy to be visited
+ // from multiple hosts. So we need to convert it to an absolute url here.
+ target := httplib.MakeAbsoluteURL(ctx, status.TargetURL)
+
payload := api.CommitStatusPayload{
Context: status.Context,
CreatedAt: status.CreatedUnix.AsTime().UTC(),
@@ -881,7 +923,7 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod
ID: status.ID,
SHA: commit.Sha1,
State: status.State.String(),
- TargetURL: status.TargetURL,
+ TargetURL: target,
Commit: apiCommit,
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
@@ -924,10 +966,90 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
return
}
+ var org *api.Organization
+ if pd.Owner.IsOrganization() {
+ org = convert.ToOrganization(ctx, organization.OrgFromUser(pd.Owner))
+ }
+
if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{
- Action: action,
- Package: apiPackage,
- Sender: convert.ToUser(ctx, sender, nil),
+ Action: action,
+ Package: apiPackage,
+ Organization: org,
+ Sender: convert.ToUser(ctx, sender, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
+ source := EventSource{
+ Repository: repo,
+ Owner: repo.Owner,
+ }
+
+ var org *api.Organization
+ if repo.Owner.IsOrganization() {
+ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+ }
+
+ status, _ := convert.ToActionsStatus(job.Status)
+
+ convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
+ if err != nil {
+ log.Error("ToActionWorkflowJob: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
+ Action: status,
+ WorkflowJob: convertedJob,
+ Organization: org,
+ Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
+ Sender: convert.ToUser(ctx, sender, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+ source := EventSource{
+ Repository: repo,
+ Owner: repo.Owner,
+ }
+
+ var org *api.Organization
+ if repo.Owner.IsOrganization() {
+ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+ }
+
+ status := convert.ToWorkflowRunAction(run.Status)
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, repo)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer gitRepo.Close()
+
+ convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
+ if err != nil {
+ log.Error("GetActionWorkflow: %v", err)
+ return
+ }
+
+ convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
+ if err != nil {
+ log.Error("ToActionWorkflowRun: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{
+ Action: status,
+ Workflow: convertedWorkflow,
+ WorkflowRun: convertedRun,
+ Organization: org,
+ Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
+ Sender: convert.ToUser(ctx, sender, nil),
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 4d809ab3a6..e6a00b0293 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -110,6 +110,18 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e
return PackagistPayload{}, nil
}
+func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
+}
+
+func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
+}
+
+func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
+}
+
func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &PackagistMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
@@ -120,3 +132,7 @@ func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook
}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest)
+}
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index f47807fa6e..4e77f29edc 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -164,7 +163,7 @@ func TestPackagistJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+ req, reqBody, err := newPackagistRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
@@ -199,7 +198,7 @@ func TestPackagistEmptyPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+ req, reqBody, err := newPackagistRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
@@ -211,5 +210,5 @@ func TestPackagistEmptyPayload(t *testing.T) {
var body PackagistPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
- assert.Equal(t, "", body.PackagistRepository.URL)
+ assert.Empty(t, body.PackagistRepository.URL)
}
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index ab280a25b6..b607bf3250 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -28,6 +28,9 @@ type payloadConvertor[T any] interface {
Release(*api.ReleasePayload) (T, error)
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
+ Status(*api.CommitStatusPayload) (T, error)
+ WorkflowRun(*api.WorkflowRunPayload) (T, error)
+ WorkflowJob(*api.WorkflowJobPayload) (T, error)
}
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
@@ -77,6 +80,12 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
return convertUnmarshalledJSON(rc.Package, data)
+ case webhook_module.HookEventStatus:
+ return convertUnmarshalledJSON(rc.Status, data)
+ case webhook_module.HookEventWorkflowRun:
+ return convertUnmarshalledJSON(rc.WorkflowRun, data)
+ case webhook_module.HookEventWorkflowJob:
+ return convertUnmarshalledJSON(rc.WorkflowJob, data)
}
return t, fmt.Errorf("newPayload unsupported event: %s", event)
}
@@ -86,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
if err != nil {
return nil, nil, err
}
+ return prepareJSONRequest(payload, w, t, withDefaultHeaders)
+}
+func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
@@ -104,7 +116,7 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
req.Header.Set("Content-Type", "application/json")
if withDefaultHeaders {
- return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), w, t, body)
}
return req, body, nil
}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index c905e7a89f..3d645a55d0 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -84,9 +84,9 @@ func SlackLinkFormatter(url, text string) string {
// SlackLinkToRef slack-formatter link to a repo ref
func SlackLinkToRef(repoURL, ref string) string {
// FIXME: SHA1 hardcoded here
- url := git.RefURL(repoURL, ref)
- refName := git.RefName(ref).ShortName()
- return SlackLinkFormatter(url, refName)
+ refName := git.RefName(ref)
+ url := repoURL + "/src/" + refName.RefWebLinkPath()
+ return SlackLinkFormatter(url, refName.ShortName())
}
// Create implements payloadConvertor Create method
@@ -167,6 +167,24 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
return s.createPayload(text, nil), nil
}
+func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) {
+ text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
+func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
+func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
// Push implements payloadConvertor Push method
func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
// n new commits
@@ -295,6 +313,10 @@ func newSlackRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mod
return newJSONRequest(pc, w, t, true)
}
+func init() {
+ RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest)
+}
+
var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
// IsValidSlackChannel validates a channel name conforms to what slack expects:
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index 7ebf16aba2..839ed6f770 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -178,7 +177,7 @@ func TestSlackJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newSlackRequest(context.Background(), hook, task)
+ req, reqBody, err := newSlackRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index e54d6f2947..fdd428b45c 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -174,10 +174,28 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
return createTelegramPayloadHTML(text), nil
}
+func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) {
+ text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayloadHTML(text), nil
+}
+
+func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayloadHTML(text), nil
+}
+
+func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayloadHTML(text), nil
+}
+
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
// https://core.telegram.org/bots/api#formatting-options
return TelegramPayload{
- Message: strings.TrimSpace(markup.Sanitize(msgHTML)),
+ Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))),
ParseMode: "HTML",
DisableWebPreview: true,
}
@@ -187,3 +205,7 @@ func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
var pc payloadConvertor[TelegramPayload] = telegramConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest)
+}
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 7ba81f1564..3fa8e27836 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -4,7 +4,6 @@
package webhook
import (
- "context"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -195,7 +194,7 @@ func TestTelegramJSONPayload(t *testing.T) {
PayloadVersion: 2,
}
- req, reqBody, err := newTelegramRequest(context.Background(), hook, task)
+ req, reqBody, err := newTelegramRequest(t.Context(), hook, task)
require.NotNil(t, req)
require.NotNil(t, reqBody)
require.NoError(t, err)
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index e0e8fa2fc1..182078b39d 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -27,16 +27,12 @@ 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 Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
+
+var webhookRequesters = map[webhook_module.HookType]Requester{}
+
+func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) {
+ webhookRequesters[hookType] = requester
}
// IsValidHookTaskType returns true if a webhook registered
@@ -137,14 +133,8 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
return nil
}
- for _, e := range w.EventCheckers() {
- if event == e.Type {
- if !e.Has() {
- return nil
- }
-
- break
- }
+ if !w.HasEvent(event) {
+ return nil
}
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 63cbce1771..5a805347e3 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -9,11 +9,16 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
webhook_module "code.gitea.io/gitea/modules/webhook"
+ "code.gitea.io/gitea/services/convert"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestWebhook_GetSlackHook(t *testing.T) {
@@ -77,3 +82,12 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
unittest.AssertNotExistsBean(t, hookTask)
}
}
+
+func TestWebhookUserMail(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
+ assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
+}
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 1d8c1d7dac..1875317406 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -175,7 +175,29 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
return newWechatworkMarkdownPayload(text), nil
}
+func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
+func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
+func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
+ text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
return newJSONRequest(pc, w, t, true)
}
+
+func init() {
+ RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest)
+}