From b09920a53737c848717210eaad565623c004f121 Mon Sep 17 00:00:00 2001 From: Shudhanshu Singh Date: Thu, 2 Jul 2026 20:44:48 +0530 Subject: [PATCH] feat(webhook): support Telegram Bot API 10.1 Rich Messages (#38298) Upgrades Gitea's Telegram webhook integration to support Telegram Bot API 10.1 (June 2026 release). This enables Gitea webhooks to take advantage of rich formatted messages (tables, nested blocks, collapsible details, etc.) by routing them through the /sendRichMessage endpoint with the new rich_message payload structure. Old `/sendMessage` webhook URLs are written to `/sendRichMessage` at runtime to prevent the need for database migrations Fixes https://github.com/go-gitea/gitea/issues/38118 --------- Co-authored-by: wxiaoguang --- routers/web/repo/setting/webhook.go | 2 +- services/webhook/telegram.go | 24 ++++++++++++---- services/webhook/telegram_test.go | 44 ++++++++++++++--------------- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index c52f5fc4b14..4d3120618f4 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -429,7 +429,7 @@ func telegramHookParams(ctx *context.Context) webhookParams { return webhookParams{ Type: webhook_module.TELEGRAM, - URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendRichMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), ContentType: webhook.ContentTypeJSON, WebhookForm: form.WebhookForm, Meta: &webhook_service.TelegramMeta{ diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 99a0b417b8a..cae9a883c79 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -8,6 +8,7 @@ import ( "fmt" "html" "net/http" + "net/url" "strings" webhook_model "gitea.dev/models/webhook" @@ -23,9 +24,12 @@ import ( type ( // TelegramPayload represents TelegramPayload struct { - Message string `json:"text"` - ParseMode string `json:"parse_mode"` - DisableWebPreview bool `json:"disable_web_page_preview"` + RichMessage InputRichMessage `json:"rich_message"` + } + + // InputRichMessage represents input rich message + InputRichMessage struct { + HTML string `json:"html"` } // TelegramMeta contains the telegram metadata @@ -195,13 +199,21 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ - Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))), - ParseMode: "HTML", - DisableWebPreview: true, + RichMessage: InputRichMessage{ + HTML: strings.TrimSpace(string(markup.Sanitize(msgHTML))), + }, } } func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + u, err := url.Parse(w.URL) + if err != nil { + return nil, nil, err + } + if urlPrefix, ok := strings.CutSuffix(u.Path, "/sendMessage"); ok { + u.Path = urlPrefix + "/sendRichMessage" + w.URL = u.String() + } var pc payloadConvertor[TelegramPayload] = telegramConvertor{} return newJSONRequest(pc, w, t, true) } diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go index 7c8209577e9..3e615af8140 100644 --- a/services/webhook/telegram_test.go +++ b/services/webhook/telegram_test.go @@ -21,9 +21,9 @@ func TestTelegramPayload(t *testing.T) { t.Run("Correct webhook params", func(t *testing.T) { p := createTelegramPayloadHTML(`testMsg `) assert.Equal(t, TelegramPayload{ - Message: `testMsg`, - ParseMode: "HTML", - DisableWebPreview: true, + RichMessage: InputRichMessage{ + HTML: `testMsg`, + }, }, p) }) @@ -33,7 +33,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Create(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] branch test created`, pl.Message) + assert.Equal(t, `[test/repo] branch test created`, pl.RichMessage.HTML) }) t.Run("Delete", func(t *testing.T) { @@ -42,7 +42,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Delete(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] branch test deleted`, pl.Message) + assert.Equal(t, `[test/repo] branch test deleted`, pl.RichMessage.HTML) }) t.Run("Fork", func(t *testing.T) { @@ -51,7 +51,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Fork(p) require.NoError(t, err) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Message) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.RichMessage.HTML) }) t.Run("Push", func(t *testing.T) { @@ -62,7 +62,7 @@ func TestTelegramPayload(t *testing.T) { assert.Equal(t, `[test/repo:test] 2 new commits [2020558] commit message - user1 -[2020558] commit message - user1`, pl.Message) +[2020558] commit message - user1`, pl.RichMessage.HTML) }) t.Run("Issue", func(t *testing.T) { @@ -74,13 +74,13 @@ func TestTelegramPayload(t *testing.T) { assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1 -issue body`, pl.Message) +issue body`, pl.RichMessage.HTML) p.Action = api.HookIssueClosed pl, err = tc.Issue(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.Message) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.RichMessage.HTML) }) t.Run("IssueComment", func(t *testing.T) { @@ -90,7 +90,7 @@ issue body`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1 -more info needed`, pl.Message) +more info needed`, pl.RichMessage.HTML) }) t.Run("PullRequest", func(t *testing.T) { @@ -100,7 +100,7 @@ more info needed`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1 -fixes bug #2`, pl.Message) +fixes bug #2`, pl.RichMessage.HTML) }) t.Run("PullRequestComment", func(t *testing.T) { @@ -110,7 +110,7 @@ fixes bug #2`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1 -changes requested`, pl.Message) +changes requested`, pl.RichMessage.HTML) }) t.Run("Review", func(t *testing.T) { @@ -121,7 +121,7 @@ changes requested`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug -good job`, pl.Message) +good job`, pl.RichMessage.HTML) }) t.Run("Repository", func(t *testing.T) { @@ -130,7 +130,7 @@ good job`, pl.Message) pl, err := tc.Repository(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Repository created`, pl.Message) + assert.Equal(t, `[test/repo] Repository created`, pl.RichMessage.HTML) }) t.Run("Package", func(t *testing.T) { @@ -139,7 +139,7 @@ good job`, pl.Message) pl, err := tc.Package(p) require.NoError(t, err) - assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.Message) + assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.RichMessage.HTML) }) t.Run("Wiki", func(t *testing.T) { @@ -149,19 +149,19 @@ good job`, pl.Message) pl, err := tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.Message) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.RichMessage.HTML) p.Action = api.HookWikiEdited pl, err = tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.Message) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.RichMessage.HTML) p.Action = api.HookWikiDeleted pl, err = tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.Message) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.RichMessage.HTML) }) t.Run("Release", func(t *testing.T) { @@ -170,7 +170,7 @@ good job`, pl.Message) pl, err := tc.Release(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.Message) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.RichMessage.HTML) }) } @@ -183,7 +183,7 @@ func TestTelegramJSONPayload(t *testing.T) { RepoID: 3, IsActive: true, Type: webhook_module.TELEGRAM, - URL: "https://telegram.example.com/", + URL: "https://telegram.example.com/sendMessage", Meta: ``, HTTPMethod: "POST", } @@ -200,7 +200,7 @@ func TestTelegramJSONPayload(t *testing.T) { require.NoError(t, err) assert.Equal(t, "POST", req.Method) - assert.Equal(t, "https://telegram.example.com/", req.URL.String()) + assert.Equal(t, "https://telegram.example.com/sendRichMessage", 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 TelegramPayload @@ -208,5 +208,5 @@ func TestTelegramJSONPayload(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `[test/repo:test] 2 new commits [2020558] commit message - user1 -[2020558] commit message - user1`, body.Message) +[2020558] commit message - user1`, body.RichMessage.HTML) }