FILE / ScuroNeko/Laniakea

cmd_generator.go

Исходный файл и его история в репозитории.
FILE 4d95bd05746a4f16d360080afe904046bb8e38fb
Files
Laniakea/cmd_generator.go
T
ScuroNeko f03a081ed6
Golang lint / lint (pull_request) Successful in 1m20s
Golang lint / lint (push) Successful in 4m8s
(new): rich message support
(fix): runtime reliability
(tests): regression coverage
(doc): v1.1 release notes
2026-08-12 16:34:44 +03:00

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
}