fix(wheelofnames): pad spoiler result to longest option so length can't leak winner

This commit is contained in:
2026-08-06 10:54:04 +07:00
parent 15f7e50305
commit c4c32ba716
2 changed files with 33 additions and 7 deletions
+15 -3
View File
@@ -150,7 +150,7 @@ func TestWheelOfNames_UsageWhenMissingOptions(t *testing.T) {
}
func TestWheelOfNames_ResultCaptionEscapesHTML(t *testing.T) {
got := wheelResultCaption(`<Alice & Bob>`)
got := wheelResultCaption([]string{`<Alice & Bob>`}, 0)
want := `Result: <span class="tg-spoiler">&lt;Alice &amp; Bob&gt;</span>`
if got != want {
t.Fatalf("wheelResultCaption() = %q, want %q", got, want)
@@ -158,13 +158,25 @@ func TestWheelOfNames_ResultCaptionEscapesHTML(t *testing.T) {
}
func TestWheelOfNames_ResultCaptionTruncatesLongResult(t *testing.T) {
got := wheelResultCaption(strings.Repeat("a", wheelResultCaptionMaxRunes+1))
got := wheelResultCaption([]string{strings.Repeat("a", wheelResultCaptionMaxRunes+1)}, 0)
want := `Result: <span class="tg-spoiler">` + strings.Repeat("a", wheelResultCaptionMaxRunes) + `...</span>`
if got != want {
t.Fatalf("wheelResultCaption() length = %d, want truncated caption length %d", len(got), len(want))
}
}
func TestWheelOfNames_ResultCaptionPadsShortWinnerToLongestOption(t *testing.T) {
options := []string{"Bob", "Alexandria"}
got := wheelResultCaption(options, 0)
want := `Result: <span class="tg-spoiler">.......Bob</span>`
if got != want {
t.Fatalf("wheelResultCaption() = %q, want %q", got, want)
}
if longest := wheelResultCaption(options, 1); len([]rune(longest)) != len([]rune(got)) {
t.Fatalf("caption lengths differ: winner %q vs %q", got, longest)
}
}
func TestWheelOfNames_UsesRemoteAPIWhenConfigured(t *testing.T) {
var got wheelAPIRequest
var gotAuthorization string
@@ -206,7 +218,7 @@ func TestWheelOfNames_UsesRemoteAPIWhenConfigured(t *testing.T) {
if call.Method != "sendAnimation" {
t.Fatalf("method = %q, want sendAnimation", call.Method)
}
wantCaption := wheelResultCaption(got.Options[got.WinnerIndex])
wantCaption := wheelResultCaption(got.Options, got.WinnerIndex)
if got := call.Form["caption"]; got != wantCaption {
t.Fatalf("caption = %q, want %q", got, wantCaption)
}
+18 -4
View File
@@ -5,6 +5,7 @@ import (
"context"
"errors"
"html"
"strings"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
@@ -51,7 +52,7 @@ func wheelOfNamesCommand() modules.Command {
Duration: animation.Duration,
Width: animation.Width,
Height: animation.Height,
Caption: wheelResultCaption(options[winner]),
Caption: wheelResultCaption(options, winner),
ParseMode: models.ParseModeHTML,
})
if err != nil {
@@ -65,9 +66,22 @@ func wheelOfNamesCommand() modules.Command {
const wheelUsage = "Usage: /wheelofnames <option,...>"
func wheelResultCaption(result string) string {
result = truncateWheelResultCaption(result)
return `Result: <span class="tg-spoiler">` + html.EscapeString(result) + `</span>`
func wheelResultCaption(options []string, winner int) string {
result := truncateWheelResultCaption(options[winner])
padding := wheelCaptionPadding(options, len([]rune(result)))
return `Result: <span class="tg-spoiler">` + html.EscapeString(padding+result) + `</span>`
}
// wheelCaptionPadding returns leading dots so every option renders a
// spoiler of equal length; otherwise the spoiler width reveals the winner.
func wheelCaptionPadding(options []string, resultRunes int) string {
longest := resultRunes
for _, option := range options {
if n := len([]rune(truncateWheelResultCaption(option))); n > longest {
longest = n
}
}
return strings.Repeat(".", longest-resultRunes)
}
func truncateWheelResultCaption(result string) string {