FILE / ScuroNeko/Laniakea
cmd_generator.go
Исходный файл и его история в репозитории.
(fix): runtime reliability (tests): regression coverage (doc): v1.1 release notes
166 lines
5.1 KiB
Go
166 lines
5.1 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
|
)
|
|
|
|
var cmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
|
|
|
|
// ErrTooManyCommands is returned when the total number of registered commands
|
|
// exceeds Telegram's limit of 100 bot commands per bot.
|
|
//
|
|
// Telegram Bot API enforces this limit strictly. If exceeded, SetMyCommands
|
|
// will fail with a 400 error. This error helps catch the issue early during
|
|
// bot initialization.
|
|
var ErrTooManyCommands = errors.New("too many commands. max 100")
|
|
|
|
func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
|
|
desc := ""
|
|
if len(cmd.description) > 0 {
|
|
desc = cmd.description
|
|
}
|
|
|
|
var descArgs []string
|
|
for _, a := range cmd.args {
|
|
if a.required {
|
|
descArgs = append(descArgs, fmt.Sprintf("<%s>", a.text))
|
|
} else {
|
|
descArgs = append(descArgs, fmt.Sprintf("[%s]", a.text))
|
|
}
|
|
}
|
|
|
|
usage := fmt.Sprintf("Usage: /%s %s", cmd.command, strings.Join(descArgs, " "))
|
|
if desc != "" {
|
|
desc = fmt.Sprintf("%s. %s", desc, usage)
|
|
return tgapi.BotCommand{Command: cmd.command, Description: desc, IsEphemeral: cmd.isEphemeral}
|
|
}
|
|
return tgapi.BotCommand{Command: cmd.command, Description: usage, IsEphemeral: cmd.isEphemeral}
|
|
}
|
|
|
|
func checkCmdRegex(cmd string) bool { return cmdRegexp.MatchString(cmd) }
|
|
|
|
func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
|
|
commands := make([]tgapi.BotCommand, 0)
|
|
names := make([]string, 0, len(pl.commands))
|
|
for name := range pl.commands {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
cmd := pl.commands[name]
|
|
if cmd.skipAutoCmd {
|
|
continue
|
|
}
|
|
if !checkCmdRegex(cmd.command) {
|
|
continue
|
|
}
|
|
commands = append(commands, generateBotCommand(cmd))
|
|
}
|
|
return commands
|
|
}
|
|
|
|
func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
|
|
commands := make([]tgapi.BotCommand, 0)
|
|
for _, pl := range bot.plugins {
|
|
if pl.skipAutoCmd {
|
|
continue
|
|
}
|
|
commands = append(commands, gatherCommandsForPlugin(pl)...)
|
|
bot.logger.Debugln(fmt.Sprintf("Registered %d commands from plugin %s", len(pl.commands), pl.name))
|
|
}
|
|
return commands
|
|
}
|
|
|
|
// AutoGenerateCommands replaces plugin-defined commands in the private-chat,
|
|
// group-chat, and all-chat-administrators scopes.
|
|
//
|
|
// Returns ErrTooManyCommands if the total number of commands exceeds 100.
|
|
// Returns any API error from Telegram (e.g., network issues, invalid scope).
|
|
//
|
|
// Important: This method assumes the bot has been properly initialized and
|
|
// the API client is authenticated and ready.
|
|
//
|
|
// Usage:
|
|
//
|
|
// err := bot.AutoGenerateCommands()
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
func (bot *Bot[T]) AutoGenerateCommands() error {
|
|
return bot.AutoGenerateCommandsWithContext(context.Background())
|
|
}
|
|
|
|
// AutoGenerateCommandsWithContext is the context-aware variant of AutoGenerateCommands.
|
|
func (bot *Bot[T]) AutoGenerateCommandsWithContext(ctx context.Context) error {
|
|
commands := gatherCommands(bot)
|
|
if len(commands) > 100 {
|
|
return ErrTooManyCommands
|
|
}
|
|
|
|
// Register commands for each scope
|
|
scopes := []tgapi.BotCommandScope{
|
|
{Type: tgapi.BotCommandScopePrivateType},
|
|
{Type: tgapi.BotCommandScopeGroupType},
|
|
{Type: tgapi.BotCommandScopeAllChatAdministratorsType},
|
|
}
|
|
|
|
for i := range scopes {
|
|
if err := bot.setCommandsForScope(ctx, &scopes[i], commands); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AutoGenerateCommandsForScope registers all plugin-defined commands with Telegram's Bot API
|
|
// for the specified command scope. A nil scope selects Telegram's default scope.
|
|
//
|
|
// The scope parameter defines where the commands should be available (e.g., private chats,
|
|
// group chats, chat administrators). See tgapi.BotCommandScope and its predefined types.
|
|
//
|
|
// Returns ErrTooManyCommands if the total number of commands exceeds 100.
|
|
// Returns any API error from Telegram (e.g., network issues, invalid scope).
|
|
//
|
|
// Usage:
|
|
//
|
|
// privateScope := &tgapi.BotCommandScope{Type: tgapi.BotCommandScopePrivateType}
|
|
// if err := bot.AutoGenerateCommandsForScope(privateScope); err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
func (bot *Bot[T]) AutoGenerateCommandsForScope(scope *tgapi.BotCommandScope) error {
|
|
return bot.AutoGenerateCommandsForScopeWithContext(context.Background(), scope)
|
|
}
|
|
|
|
// AutoGenerateCommandsForScopeWithContext is the context-aware variant of
|
|
// AutoGenerateCommandsForScope.
|
|
func (bot *Bot[T]) AutoGenerateCommandsForScopeWithContext(ctx context.Context, scope *tgapi.BotCommandScope) error {
|
|
commands := gatherCommands(bot)
|
|
if len(commands) > 100 {
|
|
return ErrTooManyCommands
|
|
}
|
|
return bot.setCommandsForScope(ctx, scope, commands)
|
|
}
|
|
|
|
func (bot *Bot[T]) setCommandsForScope(ctx context.Context, scope *tgapi.BotCommandScope, commands []tgapi.BotCommand) error {
|
|
if len(commands) > 100 {
|
|
return ErrTooManyCommands
|
|
}
|
|
_, err := bot.api.SetMyCommandsWithContext(ctx, tgapi.SetMyCommands{Scope: scope, Commands: commands})
|
|
if err != nil {
|
|
scopeType := tgapi.BotCommandScopeDefaultType
|
|
if scope != nil {
|
|
scopeType = scope.Type
|
|
}
|
|
return fmt.Errorf("failed to set commands for scope %q: %w", scopeType, err)
|
|
}
|
|
return nil
|
|
}
|