diff --git a/pkg/channels/telegram/command_registration.go b/pkg/channels/telegram/command_registration.go new file mode 100644 index 000000000..24b7031e4 --- /dev/null +++ b/pkg/channels/telegram/command_registration.go @@ -0,0 +1,84 @@ +package telegram + +import ( + "context" + "time" + + "github.com/mymmrac/telego" + + "github.com/sipeed/picoclaw/pkg/commands" + "github.com/sipeed/picoclaw/pkg/logger" +) + +var commandRegistrationBackoff = []time.Duration{ + 5 * time.Second, + 15 * time.Second, + 60 * time.Second, + 5 * time.Minute, + 10 * time.Minute, +} + +// RegisterCommands registers bot commands on Telegram platform. +func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.Definition) error { + botCommands := make([]telego.BotCommand, 0, len(defs)) + for _, def := range defs { + if def.Name == "" || def.Description == "" { + continue + } + botCommands = append(botCommands, telego.BotCommand{ + Command: def.Name, + Description: def.Description, + }) + } + + return c.bot.SetMyCommands(ctx, &telego.SetMyCommandsParams{ + Commands: botCommands, + }) +} + +func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []commands.Definition) { + if len(defs) == 0 { + return + } + + register := c.registerFunc + if register == nil { + register = c.RegisterCommands + } + + regCtx, cancel := context.WithCancel(ctx) + c.commandRegCancel = cancel + + go func() { + attempt := 0 + for { + err := register(regCtx, defs) + if err == nil { + logger.InfoCF("telegram", "Telegram commands registered", map[string]any{ + "count": len(defs), + }) + return + } + + delay := commandRegistrationBackoff[minInt(attempt, len(commandRegistrationBackoff)-1)] + logger.WarnCF("telegram", "Telegram command registration failed; will retry", map[string]any{ + "error": err.Error(), + "retry_after": delay.String(), + }) + attempt++ + + select { + case <-regCtx.Done(): + return + case <-time.After(delay): + } + } + }() +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/pkg/channels/telegram/command_registration_test.go b/pkg/channels/telegram/command_registration_test.go new file mode 100644 index 000000000..39237ab95 --- /dev/null +++ b/pkg/channels/telegram/command_registration_test.go @@ -0,0 +1,30 @@ +package telegram + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/sipeed/picoclaw/pkg/commands" +) + +func TestStartCommandRegistration_DoesNotBlock(t *testing.T) { + ch := &TelegramChannel{} + started := make(chan struct{}, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ch.registerFunc = func(context.Context, []commands.Definition) error { + started <- struct{}{} + return errors.New("temporary failure") + } + + ch.startCommandRegistration(ctx, []commands.Definition{{Name: "help"}}) + + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("registration did not start asynchronously") + } +} diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index a11cf53b8..0e1949ab6 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -18,6 +18,7 @@ import ( "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" + "github.com/sipeed/picoclaw/pkg/commands" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/identity" "github.com/sipeed/picoclaw/pkg/logger" @@ -47,6 +48,9 @@ type TelegramChannel struct { chatIDs map[string]int64 ctx context.Context cancel context.CancelFunc + + registerFunc func(context.Context, []commands.Definition) error + commandRegCancel context.CancelFunc } func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) { @@ -141,6 +145,8 @@ func (c *TelegramChannel) Start(ctx context.Context) error { "username": c.bot.Username(), }) + c.startCommandRegistration(c.ctx, commands.NewRegistry(commands.BuiltinDefinitions(c.config)).ForChannel("telegram")) + go bh.Start() return nil @@ -159,6 +165,9 @@ func (c *TelegramChannel) Stop(ctx context.Context) error { if c.cancel != nil { c.cancel() } + if c.commandRegCancel != nil { + c.commandRegCancel() + } return nil }