From 18a5bfcdb05b69aa35e95e5cbd25e23f9fadb085 Mon Sep 17 00:00:00 2001 From: Nevyana Angelova Date: Tue, 5 May 2026 16:20:14 +0300 Subject: [PATCH] MM-68364: Truncate oversized GitHub plugin DM posts to avoid silent drops --- server/plugin/plugin.go | 18 +++++++++++++++++- server/plugin/plugin_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 2c0ff42ad..8f8c204cc 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -15,6 +15,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/google/go-github/v54/github" "github.com/gorilla/mux" @@ -957,7 +958,7 @@ func (p *Plugin) CreateBotDMPost(userID, message, postType string) { post := &model.Post{ UserId: p.BotUserID, ChannelId: channel.Id, - Message: message, + Message: truncatePostMessage(message), Type: postType, } @@ -967,6 +968,21 @@ func (p *Plugin) CreateBotDMPost(userID, message, postType string) { } } +func truncatePostMessage(message string) string { + const truncationMarker = "\n\n_… message truncated_" + + if utf8.RuneCountInString(message) <= model.PostMessageMaxRunesV2 { + return message + } + + keep := model.PostMessageMaxRunesV2 - utf8.RuneCountInString(truncationMarker) + if keep <= 0 { + return string([]rune(truncationMarker)[:model.PostMessageMaxRunesV2]) + } + + return string([]rune(message)[:keep]) + truncationMarker +} + func (p *Plugin) CheckIfDuplicateDailySummary(userID, text string) (bool, error) { previousSummary, err := p.GetDailySummaryText(userID) if err != nil { diff --git a/server/plugin/plugin_test.go b/server/plugin/plugin_test.go index 0791c97cc..46c6b24f4 100644 --- a/server/plugin/plugin_test.go +++ b/server/plugin/plugin_test.go @@ -5,7 +5,9 @@ package plugin import ( "encoding/json" + "strings" "testing" + "unicode/utf8" "github.com/golang/mock/gomock" "github.com/pkg/errors" @@ -374,3 +376,33 @@ func TestForceDisconnectUser_DeleteErrors(t *testing.T) { api.AssertExpectations(t) } + +func TestTruncatePostMessage(t *testing.T) { + t.Run("short message is returned unchanged", func(t *testing.T) { + msg := "hello world" + require.Equal(t, msg, truncatePostMessage(msg)) + }) + + t.Run("message at the rune limit is returned unchanged", func(t *testing.T) { + msg := strings.Repeat("a", model.PostMessageMaxRunesV2) + require.Equal(t, msg, truncatePostMessage(msg)) + }) + + t.Run("oversized ASCII message is truncated and marker appended", func(t *testing.T) { + msg := strings.Repeat("a", model.PostMessageMaxRunesV2+5_000) + out := truncatePostMessage(msg) + + require.LessOrEqual(t, utf8.RuneCountInString(out), model.PostMessageMaxRunesV2) + require.True(t, strings.HasSuffix(out, "_… message truncated_")) + }) + + t.Run("oversized multibyte message keeps rune boundaries", func(t *testing.T) { + // Each rune is 3 bytes, so byte-based truncation would corrupt the output. + msg := strings.Repeat("✓", model.PostMessageMaxRunesV2+1_000) + out := truncatePostMessage(msg) + + require.LessOrEqual(t, utf8.RuneCountInString(out), model.PostMessageMaxRunesV2) + require.True(t, utf8.ValidString(out), "truncated output should remain valid UTF-8") + require.True(t, strings.HasSuffix(out, "_… message truncated_")) + }) +}