mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-25 00:22:56 +00:00
feat(server): register command menu on startup
This commit is contained in:
@@ -32,7 +32,7 @@ internal/modules/ Module framework, registry, dispatchers, modules
|
||||
internal/storage/ typed DocStore[T] (Provider + Typed); mongodb runtime + memory (tests). Values persist as flattened native BSON root documents
|
||||
internal/ai/ Gemini client (used by twentyq)
|
||||
compose.yml Coolify self-host stack (single bot service)
|
||||
telegram-commands.json Telegram command menu source
|
||||
telegram-commands.json Manual Telegram command menu source
|
||||
docs/deploy-coolify-selfhosted.md Self-host deploy and operations guide
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
)
|
||||
|
||||
const commandMenuTimeout = 8 * time.Second
|
||||
|
||||
// botCommandMenu builds the Telegram command menu from the loaded public
|
||||
// commands. It intentionally follows registry module order so MODULES controls
|
||||
// both enabled commands and their menu grouping.
|
||||
func botCommandMenu(reg *modules.Registry) []models.BotCommand {
|
||||
if reg == nil {
|
||||
return nil
|
||||
}
|
||||
var out []models.BotCommand
|
||||
for _, mod := range reg.Modules {
|
||||
for _, cmd := range mod.Commands {
|
||||
if cmd.Visibility != modules.VisibilityPublic {
|
||||
continue
|
||||
}
|
||||
out = append(out, models.BotCommand{
|
||||
Command: cmd.Name,
|
||||
Description: cmd.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// registerCommandMenu publishes the menu to Telegram. It is best-effort at
|
||||
// startup: callers should log failures and keep the bot running.
|
||||
func registerCommandMenu(ctx context.Context, b *bot.Bot, reg *modules.Registry) (int, error) {
|
||||
cmds := botCommandMenu(reg)
|
||||
menuCtx, cancel := context.WithTimeout(ctx, commandMenuTimeout)
|
||||
defer cancel()
|
||||
|
||||
ok, err := b.SetMyCommands(menuCtx, &bot.SetMyCommandsParams{Commands: cmds})
|
||||
if err != nil {
|
||||
return len(cmds), err
|
||||
}
|
||||
if !ok {
|
||||
return len(cmds), errors.New("telegram setMyCommands returned false")
|
||||
}
|
||||
return len(cmds), nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/modules"
|
||||
"github.com/tiennm99/miti99bot/internal/testutil"
|
||||
)
|
||||
|
||||
func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) {
|
||||
reg := &modules.Registry{
|
||||
Modules: []modules.Module{
|
||||
{
|
||||
Name: "beta",
|
||||
Commands: []modules.Command{
|
||||
{Name: "beta_public", Description: "Beta public", Visibility: modules.VisibilityPublic},
|
||||
{Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "alpha",
|
||||
Commands: []modules.Command{
|
||||
{Name: "alpha_public", Description: "Alpha public", Visibility: modules.VisibilityPublic},
|
||||
{Name: "alpha_protected", Description: "Alpha protected", Visibility: modules.VisibilityProtected},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := botCommandMenu(reg)
|
||||
want := []models.BotCommand{
|
||||
{Command: "beta_public", Description: "Beta public"},
|
||||
{Command: "alpha_public", Description: "Alpha public"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("commands = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("commands[%d] = %+v, want %+v", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterCommandMenu_CallsTelegramSetMyCommands(t *testing.T) {
|
||||
reg := &modules.Registry{
|
||||
Modules: []modules.Module{{
|
||||
Name: "demo",
|
||||
Commands: []modules.Command{{
|
||||
Name: "demo",
|
||||
Description: "Demo command",
|
||||
Visibility: modules.VisibilityPublic,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
rb := testutil.NewRecordingBot(t)
|
||||
|
||||
n, err := registerCommandMenu(context.Background(), rb.Bot, reg)
|
||||
if err != nil {
|
||||
t.Fatalf("registerCommandMenu: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("registered count = %d, want 1", n)
|
||||
}
|
||||
|
||||
call := rb.LastSent()
|
||||
if call.Method != "setMyCommands" {
|
||||
t.Fatalf("method = %q, want setMyCommands", call.Method)
|
||||
}
|
||||
var cmds []models.BotCommand
|
||||
if err := json.Unmarshal([]byte(call.Form["commands"]), &cmds); err != nil {
|
||||
t.Fatalf("decode commands form field: %v; raw=%q", err, call.Form["commands"])
|
||||
}
|
||||
if len(cmds) != 1 || cmds[0].Command != "demo" || cmds[0].Description != "Demo command" {
|
||||
t.Fatalf("commands payload = %+v, want demo command", cmds)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,11 @@ func main() {
|
||||
"modules", len(reg.Modules),
|
||||
"commands", len(reg.AllCommands),
|
||||
"crons", len(reg.Crons()))
|
||||
if n, err := registerCommandMenu(rootCtx, b, reg); err != nil {
|
||||
log.Warn("telegram command menu registration failed", "commands", n, "err", err)
|
||||
} else {
|
||||
log.Info("telegram command menu registered", "commands", n)
|
||||
}
|
||||
|
||||
// In-process cron scheduler runs unconditionally so the long-lived container
|
||||
// fires module crons (e.g. the lolschedule daily push) on their Schedule.
|
||||
|
||||
@@ -97,9 +97,10 @@ Copy [`.env.example`](../.env.example) → `.env` (gitignored) and fill in.
|
||||
auto-reconnects on the next op); a DB outage will not auto-restart the
|
||||
container — accepted trade-off.
|
||||
|
||||
## 3. Register the command menu
|
||||
## 3. Command menu
|
||||
|
||||
Long polling needs no webhook registration — only the command menu:
|
||||
The bot registers its Telegram command menu from loaded public modules on
|
||||
startup. The manual target remains useful for repairs or local experiments:
|
||||
|
||||
```sh
|
||||
TELEGRAM_BOT_TOKEN=… make telegram-commands
|
||||
|
||||
Reference in New Issue
Block a user