(new): v1.2 release
Golang lint / lint (push) Successful in 11m32s

This commit is contained in:
2026-08-19 14:58:25 +03:00
parent f03a081ed6
commit 29b208eeec
79 changed files with 7301 additions and 2060 deletions
+90 -10
View File
@@ -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
}