From 7cf8b402baec51ad12580236d0d6da268fb3bd29 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 12 Apr 2026 16:31:20 +0200 Subject: [PATCH 1/8] fix encoding --- routers/web/repo/setting/webhook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index b0f3a5cfee869..5eda32515ce37 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -455,7 +455,7 @@ func matrixHookParams(ctx *context.Context) webhookParams { return webhookParams{ Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, strings.ReplaceAll(url.PathEscape(form.RoomID), "%21", "!")), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, From 16801d1fdf84e2754e7f3eefd7331d5f44d8832b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 12 Apr 2026 16:49:46 +0200 Subject: [PATCH 2/8] Also restore %3A to : in Matrix room ID URL path Matrix room IDs are of the form !localpart:server. Both ! and : are valid unencoded path segment characters per RFC 3986, but url.PathEscape encodes both. Restore : alongside ! so the full room ID is preserved. Co-Authored-By: Claude Sonnet 4.6 --- routers/web/repo/setting/webhook.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 5eda32515ce37..0d76522dcc543 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -454,8 +454,13 @@ func matrixHookParams(ctx *context.Context) webhookParams { form := web.GetForm(ctx).(*forms.NewMatrixHookForm) return webhookParams{ - Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, strings.ReplaceAll(url.PathEscape(form.RoomID), "%21", "!")), + Type: webhook_module.MATRIX, + // Matrix room IDs are of the form "!localpart:server", where "!" and ":" are + // valid unencoded path segment characters per RFC 3986, but url.PathEscape + // encodes them. Restore them so Matrix homeservers can recognise the room ID. + // See https://spec.matrix.org/latest/appendices/#room-ids + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, + strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(form.RoomID))), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, From 82855e281206e084355a87c9796b192c0241efa7 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 12 Apr 2026 16:59:18 +0200 Subject: [PATCH 3/8] restore --- routers/web/repo/setting/webhook.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 0d76522dcc543..01b0976182585 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -455,12 +455,9 @@ func matrixHookParams(ctx *context.Context) webhookParams { return webhookParams{ Type: webhook_module.MATRIX, - // Matrix room IDs are of the form "!localpart:server", where "!" and ":" are - // valid unencoded path segment characters per RFC 3986, but url.PathEscape - // encodes them. Restore them so Matrix homeservers can recognise the room ID. // See https://spec.matrix.org/latest/appendices/#room-ids URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, - strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(form.RoomID))), + strings.NewReplacer("%21", "!").Replace(url.PathEscape(form.RoomID))), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, From 13c9bf72443daa42f987576d3f9ee7182acd4e03 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 23:00:02 +0800 Subject: [PATCH 4/8] Apply suggestion from @wxiaoguang Signed-off-by: wxiaoguang --- routers/web/repo/setting/webhook.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 01b0976182585..21ed1bbc1202f 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -456,6 +456,8 @@ func matrixHookParams(ctx *context.Context) webhookParams { return webhookParams{ Type: webhook_module.MATRIX, // See https://spec.matrix.org/latest/appendices/#room-ids + // Demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation + // It seems that only `!` needs to be kept URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, strings.NewReplacer("%21", "!").Replace(url.PathEscape(form.RoomID))), ContentType: webhook.ContentTypeJSON, From 0d67746538ad3c341943bdb7b515ff236d2ea5e2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 23:22:33 +0800 Subject: [PATCH 5/8] fix --- routers/web/repo/setting/webhook.go | 16 ++++++++++------ routers/web/repo/setting/webhook_test.go | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 routers/web/repo/setting/webhook_test.go diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 21ed1bbc1202f..1763d8293b862 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -450,16 +450,20 @@ func MatrixHooksEditPost(ctx *context.Context) { editWebhook(ctx, matrixHookParams(ctx)) } +func matrixRoomIDEncode(roomID string) string { + // See https://spec.matrix.org/latest/appendices/#room-ids + // Demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation + // API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room + // But some of their examples also shows links like: "PUT /rooms/!roomid:domain/state/m.example.event" + return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID)) +} + func matrixHookParams(ctx *context.Context) webhookParams { form := web.GetForm(ctx).(*forms.NewMatrixHookForm) return webhookParams{ - Type: webhook_module.MATRIX, - // See https://spec.matrix.org/latest/appendices/#room-ids - // Demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation - // It seems that only `!` needs to be kept - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, - strings.NewReplacer("%21", "!").Replace(url.PathEscape(form.RoomID))), + Type: webhook_module.MATRIX, + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, diff --git a/routers/web/repo/setting/webhook_test.go b/routers/web/repo/setting/webhook_test.go new file mode 100644 index 0000000000000..0e20a29c8717d --- /dev/null +++ b/routers/web/repo/setting/webhook_test.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookMatrix(t *testing.T) { + assert.Equal(t, "!roomid:domain", matrixRoomIDEncode("!roomid:domain")) + assert.Equal(t, "!room%23id:domain", matrixRoomIDEncode("!room#id:domain")) // maybe it should never really happen in real world + +} From e87375584817ff0a860124c1994f83a2c2d2a30a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 23:24:05 +0800 Subject: [PATCH 6/8] add more comment --- routers/web/repo/setting/webhook.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1763d8293b862..ccdb9a1f68fce 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -461,6 +461,7 @@ func matrixRoomIDEncode(roomID string) string { func matrixHookParams(ctx *context.Context) webhookParams { form := web.GetForm(ctx).(*forms.NewMatrixHookForm) + // TODO: need to migrate to the latest (v3) API: https://spec.matrix.org/v1.18/client-server-api/ return webhookParams{ Type: webhook_module.MATRIX, URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)), From ddb19363fd24a64e66979b6e0f4f6280c85ad47f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 23:25:31 +0800 Subject: [PATCH 7/8] fix comment --- routers/web/repo/setting/webhook.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index ccdb9a1f68fce..8c57a68b2503d 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -452,9 +452,9 @@ func MatrixHooksEditPost(ctx *context.Context) { func matrixRoomIDEncode(roomID string) string { // See https://spec.matrix.org/latest/appendices/#room-ids - // Demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation + // Some (unrelated) demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation // API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room - // But some of their examples also shows links like: "PUT /rooms/!roomid:domain/state/m.example.event" + // Some of their examples show links like: "PUT /rooms/!roomid:domain/state/m.example.event" return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID)) } From 8322d1f6f98e58b74bef92cf900f4a01d9d02cf3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 23:38:33 +0800 Subject: [PATCH 8/8] fix lint --- routers/web/repo/setting/webhook_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/routers/web/repo/setting/webhook_test.go b/routers/web/repo/setting/webhook_test.go index 0e20a29c8717d..ca4a21e0755b5 100644 --- a/routers/web/repo/setting/webhook_test.go +++ b/routers/web/repo/setting/webhook_test.go @@ -12,5 +12,4 @@ import ( func TestWebhookMatrix(t *testing.T) { assert.Equal(t, "!roomid:domain", matrixRoomIDEncode("!roomid:domain")) assert.Equal(t, "!room%23id:domain", matrixRoomIDEncode("!room#id:domain")) // maybe it should never really happen in real world - }