diff options
Diffstat (limited to 'services/webhook')
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) +} |