mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
feat(help): split /help into several messages past Telegram's length limit
This commit is contained in:
1 parent
7cc0ead54f
commit
4607ae5f14
4 files changed
+111
-25
No files matched your search
@@ -40,8 +40,10 @@ description:
|
||||
<quantity> <ticker>. Buy VN stock at market price.
|
||||
```
|
||||
|
||||
`/help` combines the full command syntax and summary on one line. Neither
|
||||
discovery surface includes example invocations.
|
||||
`/help` combines the full command syntax and summary on one line. When the
|
||||
list exceeds Telegram's 4096-character message limit, `/help` sends it as
|
||||
several messages, breaking only between modules. Neither discovery surface
|
||||
includes example invocations.
|
||||
|
||||
Future commands must follow the
|
||||
[command parameter conventions](docs/command-parameter-conventions.md). Keep
|
||||
|
||||
@@ -138,9 +138,10 @@ func TestCommandDiscovery_AllPublicCommandsHaveSafeMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
help := moduleutil.RenderHelp(reg)
|
||||
if utf8.RuneCountInString(help) > telegramMessageMaxRunesForTest {
|
||||
t.Fatalf("/help source is %d characters, exceeds conservative Telegram limit %d", utf8.RuneCountInString(help), telegramMessageMaxRunesForTest)
|
||||
for i, help := range moduleutil.RenderHelpMessages(reg) {
|
||||
if utf8.RuneCountInString(help) > telegramMessageMaxRunesForTest {
|
||||
t.Fatalf("/help message %d source is %d characters, exceeds conservative Telegram limit %d", i+1, utf8.RuneCountInString(help), telegramMessageMaxRunesForTest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
@@ -19,17 +20,29 @@ var supportFooter = fmt.Sprintf(
|
||||
repoURL, repoURL,
|
||||
)
|
||||
|
||||
// RenderHelp produces the body of /help: each module's public commands
|
||||
// grouped under a bold module name, followed by the support footer.
|
||||
// Modules in MODULES-env order. Modules with no visible commands are omitted.
|
||||
// Protected and private commands are hidden; authorization-specific commands
|
||||
// stay discoverable only through operator knowledge, not the public help/menu.
|
||||
// helpMessageLimit is Telegram's message length cap. Telegram counts text
|
||||
// after HTML tags are parsed out, so measuring the HTML source is conservative.
|
||||
const helpMessageLimit = 4096
|
||||
|
||||
// RenderHelp produces the full body of /help: each module's public commands
|
||||
// grouped under a bold module name, followed by the support footer. It is
|
||||
// RenderHelpMessages joined back into one string.
|
||||
//
|
||||
// Exposed (capitalised) so tests can assert on the string without spinning up
|
||||
// a bot context.
|
||||
func RenderHelp(reg *modules.Registry) string {
|
||||
return strings.Join(RenderHelpMessages(reg), "\n\n")
|
||||
}
|
||||
|
||||
// RenderHelpMessages renders /help as one or more messages, each within
|
||||
// Telegram's length cap. Messages break only between module sections, and the
|
||||
// support footer ends the last one.
|
||||
// Modules in MODULES-env order. Modules with no visible commands are omitted.
|
||||
// Protected and private commands are hidden; authorization-specific commands
|
||||
// stay discoverable only through operator knowledge, not the public help/menu.
|
||||
func RenderHelpMessages(reg *modules.Registry) []string {
|
||||
if reg == nil {
|
||||
return "no commands registered\n\n" + supportFooter
|
||||
return []string{"no commands registered\n\n" + supportFooter}
|
||||
}
|
||||
|
||||
byModule := make(map[string][]modules.Command, len(reg.Modules))
|
||||
@@ -53,12 +66,30 @@ func RenderHelp(reg *modules.Registry) string {
|
||||
}
|
||||
sections = append(sections, sb.String())
|
||||
}
|
||||
|
||||
body := "no commands registered"
|
||||
if len(sections) > 0 {
|
||||
body = strings.Join(sections, "\n\n")
|
||||
if len(sections) == 0 {
|
||||
sections = []string{"no commands registered"}
|
||||
}
|
||||
return body + "\n\n" + supportFooter
|
||||
return packHelpSections(append(sections, supportFooter), helpMessageLimit)
|
||||
}
|
||||
|
||||
// packHelpSections greedily joins sections with blank lines into messages of
|
||||
// at most limit runes. A section longer than limit gets a message of its own.
|
||||
func packHelpSections(sections []string, limit int) []string {
|
||||
var messages []string
|
||||
current := ""
|
||||
for _, section := range sections {
|
||||
candidate := section
|
||||
if current != "" {
|
||||
candidate = current + "\n\n" + section
|
||||
}
|
||||
if current != "" && utf8.RuneCountInString(candidate) > limit {
|
||||
messages = append(messages, current)
|
||||
current = section
|
||||
continue
|
||||
}
|
||||
current = candidate
|
||||
}
|
||||
return append(messages, current)
|
||||
}
|
||||
|
||||
// ownerOf finds the module that registered the named command. Linear scan
|
||||
@@ -85,15 +116,18 @@ func helpCommand(reg *modules.Registry) modules.Command {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
}
|
||||
text := RenderHelp(reg)
|
||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: update.Message.Chat.ID,
|
||||
MessageThreadID: update.Message.MessageThreadID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: bot.True()},
|
||||
})
|
||||
return err
|
||||
for _, text := range RenderHelpMessages(reg) {
|
||||
if _, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: update.Message.Chat.ID,
|
||||
MessageThreadID: update.Message.MessageThreadID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeHTML,
|
||||
LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: bot.True()},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
@@ -161,3 +162,51 @@ func TestRenderHelp_NilRegistryReturnsFooterOnly(t *testing.T) {
|
||||
t.Errorf("footer missing; got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHelpMessages_SplitsBetweenModulesUnderTelegramLimit(t *testing.T) {
|
||||
factories := map[string]modules.Factory{}
|
||||
var order []string
|
||||
for i := range 12 {
|
||||
name := "mod" + string(rune('a'+i))
|
||||
var cmds []modules.Command
|
||||
for j := range 10 {
|
||||
cmds = append(cmds, modules.Command{
|
||||
Name: name + "_" + string(rune('a'+j)),
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: strings.Repeat("long description ", 2),
|
||||
Handler: helpTestNoop,
|
||||
})
|
||||
}
|
||||
factories[name] = fakeFactory(name, cmds)
|
||||
order = append(order, name)
|
||||
}
|
||||
reg, err := modules.Build(order, factories, storage.NewMemoryProvider(), modules.BuildOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
|
||||
messages := util.RenderHelpMessages(reg)
|
||||
if len(messages) < 2 {
|
||||
t.Fatalf("messages = %d, want the help split into several", len(messages))
|
||||
}
|
||||
for i, m := range messages {
|
||||
if n := utf8.RuneCountInString(m); n > 4096 {
|
||||
t.Errorf("message %d is %d runes, over 4096", i+1, n)
|
||||
}
|
||||
if !strings.HasPrefix(m, "<b>") {
|
||||
t.Errorf("message %d does not start at a module section: %q", i+1, m[:min(len(m), 40)])
|
||||
}
|
||||
if hasFooter := strings.Contains(m, "starring the repo"); hasFooter != (i == len(messages)-1) {
|
||||
t.Errorf("message %d footer present = %v", i+1, hasFooter)
|
||||
}
|
||||
}
|
||||
if got := strings.Join(messages, "\n\n"); got != util.RenderHelp(reg) {
|
||||
t.Error("joined messages differ from RenderHelp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHelpMessages_SmallHelpIsOneMessage(t *testing.T) {
|
||||
if got := util.RenderHelpMessages(nil); len(got) != 1 {
|
||||
t.Fatalf("messages = %d, want 1", len(got))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user