diff --git a/pkg/channels/telegram/command_registration.go b/pkg/channels/telegram/command_registration.go index 41f4dedce..d0befc537 100644 --- a/pkg/channels/telegram/command_registration.go +++ b/pkg/channels/telegram/command_registration.go @@ -53,6 +53,14 @@ func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []c // by temporary upstream API failures. Retry stops on success or channel shutdown. go func() { attempt := 0 + timer := time.NewTimer(0) + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + defer timer.Stop() for { err := register(regCtx, defs) if err == nil { @@ -64,15 +72,23 @@ func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []c delay := commandRegistrationBackoff[min(attempt, len(commandRegistrationBackoff)-1)] logger.WarnCF("telegram", "Telegram command registration failed; will retry", map[string]any{ - "error": err.Error(), + "error": err.Error(), "retry_after": delay.String(), }) attempt++ + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(delay) + select { case <-regCtx.Done(): return - case <-time.After(delay): + case <-timer.C: } } }() diff --git a/pkg/channels/telegram/telegram_dispatch.go b/pkg/channels/telegram/telegram_dispatch.go index 4df0f28dd..d1221acff 100644 --- a/pkg/channels/telegram/telegram_dispatch.go +++ b/pkg/channels/telegram/telegram_dispatch.go @@ -53,6 +53,11 @@ func (c *TelegramChannel) dispatchCommand(ctx context.Context, message telego.Me "error": res.Err.Error(), }) } + if res.Matched && !res.Handled { + logger.DebugCF("telegram", "Command matched without handler; passing to normal flow", map[string]any{ + "command": res.Command, + }) + } return true } diff --git a/pkg/channels/whatsapp/whatsapp.go b/pkg/channels/whatsapp/whatsapp.go index c1a95eb5f..c22d69cd3 100644 --- a/pkg/channels/whatsapp/whatsapp.go +++ b/pkg/channels/whatsapp/whatsapp.go @@ -278,6 +278,11 @@ func (c *WhatsAppChannel) tryHandleCommand( "error": res.Err.Error(), }) } + if res.Matched && !res.Handled { + logger.DebugCF("whatsapp", "Command matched without handler; passing to normal flow", map[string]any{ + "command": res.Command, + }) + } return res.Matched } diff --git a/pkg/channels/whatsapp_native/whatsapp_native.go b/pkg/channels/whatsapp_native/whatsapp_native.go index 0f093775f..3e579dea5 100644 --- a/pkg/channels/whatsapp_native/whatsapp_native.go +++ b/pkg/channels/whatsapp_native/whatsapp_native.go @@ -422,6 +422,11 @@ func (c *WhatsAppNativeChannel) tryHandleCommand( "error": res.Err.Error(), }) } + if res.Matched && !res.Handled { + logger.DebugCF("whatsapp", "Command matched without handler; passing to normal flow", map[string]any{ + "command": res.Command, + }) + } return res.Matched } diff --git a/pkg/commands/dispatcher.go b/pkg/commands/dispatcher.go index 4670def4a..177419415 100644 --- a/pkg/commands/dispatcher.go +++ b/pkg/commands/dispatcher.go @@ -37,6 +37,8 @@ func (f DispatchFunc) Dispatch(ctx context.Context, req Request) Result { return f(ctx, req) } +var commandPrefixes = []string{"/", "!"} + // NewDispatcher binds the unified parser/executor flow to one command registry. func NewDispatcher(reg *Registry) *Dispatcher { return &Dispatcher{reg: reg} @@ -52,7 +54,7 @@ func (d *Dispatcher) Dispatch(ctx context.Context, req Request) Result { } for _, def := range d.reg.Definitions() { - if def.Name != cmdName && !contains(def.Aliases, cmdName) { + if !matchesCommand(def, cmdName) { continue } if def.Handler == nil { @@ -73,24 +75,53 @@ func firstToken(input string) string { return parts[0] } -// parseCommandName accepts both "/name" and "/name@bot", then normalizes to "name". +// parseCommandName accepts "/name", "!name", and Telegram's "/name@bot", then +// normalizes to lowercase command names. func parseCommandName(input string) (string, bool) { token := firstToken(input) - if token == "" || !strings.HasPrefix(token, "/") { + if token == "" { return "", false } - name := strings.TrimPrefix(token, "/") + name, ok := trimCommandPrefix(token) + if !ok { + return "", false + } if i := strings.Index(name, "@"); i >= 0 { name = name[:i] } - name = strings.TrimSpace(name) + name = normalizeCommandName(name) if name == "" { return "", false } return name, true } +func trimCommandPrefix(token string) (string, bool) { + for _, prefix := range commandPrefixes { + if strings.HasPrefix(token, prefix) { + return strings.TrimPrefix(token, prefix), true + } + } + return "", false +} + +func normalizeCommandName(name string) string { + return strings.ToLower(strings.TrimSpace(name)) +} + +func matchesCommand(def Definition, cmdName string) bool { + if normalizeCommandName(def.Name) == cmdName { + return true + } + for _, alias := range def.Aliases { + if normalizeCommandName(alias) == cmdName { + return true + } + } + return false +} + func contains(items []string, target string) bool { for _, item := range items { if item == target { diff --git a/pkg/commands/dispatcher_test.go b/pkg/commands/dispatcher_test.go index 44626d323..d4877f5b7 100644 --- a/pkg/commands/dispatcher_test.go +++ b/pkg/commands/dispatcher_test.go @@ -59,3 +59,45 @@ func TestDispatcher_MatchTelegramMentionSyntax(t *testing.T) { t.Fatalf("dispatch result = %+v, called=%v", res, called) } } + +func TestDispatcher_MatchBangPrefix(t *testing.T) { + called := false + d := NewDispatcher(NewRegistry([]Definition{ + { + Name: "help", + Handler: func(context.Context, Request) error { + called = true + return nil + }, + }, + })) + + res := d.Dispatch(context.Background(), Request{ + Channel: "telegram", + Text: "!help", + }) + if !res.Matched || !res.Handled || !called || res.Err != nil { + t.Fatalf("dispatch result = %+v, called=%v", res, called) + } +} + +func TestDispatcher_CommandMatchingIsCaseInsensitive(t *testing.T) { + called := false + d := NewDispatcher(NewRegistry([]Definition{ + { + Name: "show", + Handler: func(context.Context, Request) error { + called = true + return nil + }, + }, + })) + + res := d.Dispatch(context.Background(), Request{ + Channel: "telegram", + Text: "/SHOW", + }) + if !res.Matched || !res.Handled || !called || res.Err != nil { + t.Fatalf("dispatch result = %+v, called=%v", res, called) + } +}