+90
-10
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
@@ -21,6 +22,39 @@ var cmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
|
||||
// bot initialization.
|
||||
var ErrTooManyCommands = errors.New("too many commands. max 100")
|
||||
|
||||
var (
|
||||
// ErrInvalidBotCommand reports a command name that Telegram would reject.
|
||||
ErrInvalidBotCommand = errors.New("invalid bot command")
|
||||
// ErrInvalidBotCommandDescription reports an empty or overlong generated description.
|
||||
ErrInvalidBotCommandDescription = errors.New("invalid bot command description")
|
||||
// ErrDuplicateBotCommand reports the same command generated by multiple plugins.
|
||||
ErrDuplicateBotCommand = errors.New("duplicate bot command")
|
||||
// ErrPartialCommandScopeUpdate reports that an earlier command scope was
|
||||
// updated before a later scope failed.
|
||||
ErrPartialCommandScopeUpdate = errors.New("partial command scope update")
|
||||
)
|
||||
|
||||
// CommandScopeUpdateError describes a failed multi-scope command update.
|
||||
// UpdatedScopes lists scopes successfully changed before FailedScope failed.
|
||||
type CommandScopeUpdateError struct {
|
||||
// UpdatedScopes contains scopes changed before the failure.
|
||||
UpdatedScopes []tgapi.BotCommandScopeType
|
||||
// FailedScope identifies the scope whose update failed.
|
||||
FailedScope tgapi.BotCommandScopeType
|
||||
// Err is the Telegram API error for FailedScope.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns a human-readable partial-update description.
|
||||
func (e *CommandScopeUpdateError) Error() string {
|
||||
return fmt.Sprintf("%v: updated %v; failed scope %q: %v", ErrPartialCommandScopeUpdate, e.UpdatedScopes, e.FailedScope, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap exposes both the partial-update sentinel and the underlying API error.
|
||||
func (e *CommandScopeUpdateError) Unwrap() []error {
|
||||
return []error{ErrPartialCommandScopeUpdate, e.Err}
|
||||
}
|
||||
|
||||
func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
|
||||
desc := ""
|
||||
if len(cmd.description) > 0 {
|
||||
@@ -36,7 +70,10 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
|
||||
}
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("Usage: /%s %s", cmd.command, strings.Join(descArgs, " "))
|
||||
usage := fmt.Sprintf("Usage: /%s", cmd.command)
|
||||
if len(descArgs) > 0 {
|
||||
usage += " " + strings.Join(descArgs, " ")
|
||||
}
|
||||
if desc != "" {
|
||||
desc = fmt.Sprintf("%s. %s", desc, usage)
|
||||
return tgapi.BotCommand{Command: cmd.command, Description: desc, IsEphemeral: cmd.isEphemeral}
|
||||
@@ -46,7 +83,7 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
|
||||
|
||||
func checkCmdRegex(cmd string) bool { return cmdRegexp.MatchString(cmd) }
|
||||
|
||||
func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
|
||||
func gatherCommandsForPlugin[T any](pl Plugin[T]) ([]tgapi.BotCommand, error) {
|
||||
commands := make([]tgapi.BotCommand, 0)
|
||||
names := make([]string, 0, len(pl.commands))
|
||||
for name := range pl.commands {
|
||||
@@ -60,23 +97,51 @@ func gatherCommandsForPlugin[T any](pl Plugin[T]) []tgapi.BotCommand {
|
||||
continue
|
||||
}
|
||||
if !checkCmdRegex(cmd.command) {
|
||||
continue
|
||||
return nil, fmt.Errorf("%w %q in plugin %q", ErrInvalidBotCommand, cmd.command, pl.name)
|
||||
}
|
||||
commands = append(commands, generateBotCommand(cmd))
|
||||
generated := generateBotCommand(cmd)
|
||||
descriptionLength := utf8.RuneCountInString(generated.Description)
|
||||
if descriptionLength < 1 || descriptionLength > 256 {
|
||||
return nil, fmt.Errorf(
|
||||
"%w for %q in plugin %q: got %d characters, want 1..256",
|
||||
ErrInvalidBotCommandDescription,
|
||||
cmd.command,
|
||||
pl.name,
|
||||
descriptionLength,
|
||||
)
|
||||
}
|
||||
commands = append(commands, generated)
|
||||
}
|
||||
return commands
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
|
||||
func gatherCommands[T any](bot *Bot[T]) ([]tgapi.BotCommand, error) {
|
||||
commands := make([]tgapi.BotCommand, 0)
|
||||
owners := make(map[string]string)
|
||||
for _, pl := range bot.plugins {
|
||||
if pl.skipAutoCmd {
|
||||
continue
|
||||
}
|
||||
commands = append(commands, gatherCommandsForPlugin(pl)...)
|
||||
pluginCommands, err := gatherCommandsForPlugin(pl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, command := range pluginCommands {
|
||||
if owner, exists := owners[command.Command]; exists {
|
||||
return nil, fmt.Errorf(
|
||||
"%w %q in plugins %q and %q",
|
||||
ErrDuplicateBotCommand,
|
||||
command.Command,
|
||||
owner,
|
||||
pl.name,
|
||||
)
|
||||
}
|
||||
owners[command.Command] = pl.name
|
||||
commands = append(commands, command)
|
||||
}
|
||||
bot.logger.Debugln(fmt.Sprintf("Registered %d commands from plugin %s", len(pl.commands), pl.name))
|
||||
}
|
||||
return commands
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// AutoGenerateCommands replaces plugin-defined commands in the private-chat,
|
||||
@@ -100,7 +165,10 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
|
||||
|
||||
// AutoGenerateCommandsWithContext is the context-aware variant of AutoGenerateCommands.
|
||||
func (bot *Bot[T]) AutoGenerateCommandsWithContext(ctx context.Context) error {
|
||||
commands := gatherCommands(bot)
|
||||
commands, err := gatherCommands(bot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(commands) > 100 {
|
||||
return ErrTooManyCommands
|
||||
}
|
||||
@@ -112,10 +180,19 @@ func (bot *Bot[T]) AutoGenerateCommandsWithContext(ctx context.Context) error {
|
||||
{Type: tgapi.BotCommandScopeAllChatAdministratorsType},
|
||||
}
|
||||
|
||||
updatedScopes := make([]tgapi.BotCommandScopeType, 0, len(scopes))
|
||||
for i := range scopes {
|
||||
if err := bot.setCommandsForScope(ctx, &scopes[i], commands); err != nil {
|
||||
if len(updatedScopes) > 0 {
|
||||
return &CommandScopeUpdateError{
|
||||
UpdatedScopes: updatedScopes,
|
||||
FailedScope: scopes[i].Type,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
updatedScopes = append(updatedScopes, scopes[i].Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -142,7 +219,10 @@ func (bot *Bot[T]) AutoGenerateCommandsForScope(scope *tgapi.BotCommandScope) er
|
||||
// AutoGenerateCommandsForScopeWithContext is the context-aware variant of
|
||||
// AutoGenerateCommandsForScope.
|
||||
func (bot *Bot[T]) AutoGenerateCommandsForScopeWithContext(ctx context.Context, scope *tgapi.BotCommandScope) error {
|
||||
commands := gatherCommands(bot)
|
||||
commands, err := gatherCommands(bot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(commands) > 100 {
|
||||
return ErrTooManyCommands
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user