(new): rich message support
Golang lint / lint (pull_request) Successful in 1m20s
Golang lint / lint (push) Successful in 4m8s

(fix): runtime reliability
(tests): regression coverage
(doc): v1.1 release notes
This commit is contained in:
2026-08-12 16:34:44 +03:00
parent 48ddf66540
commit f03a081ed6
83 changed files with 6122 additions and 1925 deletions
+32 -38
View File
@@ -1,6 +1,7 @@
package laniakea
import (
"context"
"errors"
"fmt"
"regexp"
@@ -10,7 +11,6 @@ import (
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// cmdRegexp matches command names allowed for Telegram command registration.
var cmdRegexp = regexp.MustCompile("^[_a-z0-9]{1,32}$")
// ErrTooManyCommands is returned when the total number of registered commands
@@ -39,9 +39,9 @@ func generateBotCommand[T any](cmd *Command[T]) tgapi.BotCommand {
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}
return tgapi.BotCommand{Command: cmd.command, Description: desc, IsEphemeral: cmd.isEphemeral}
}
return tgapi.BotCommand{Command: cmd.command, Description: usage}
return tgapi.BotCommand{Command: cmd.command, Description: usage, IsEphemeral: cmd.isEphemeral}
}
func checkCmdRegex(cmd string) bool { return cmdRegexp.MatchString(cmd) }
@@ -79,15 +79,8 @@ func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
return commands
}
// AutoGenerateCommands registers all plugin-defined commands with Telegram's Bot API
// across three scopes:
// - Private chats (users)
// - Group chats
// - Group administrators
//
// It first deletes existing commands to ensure a clean state, then sets the new
// set of commands for all scopes. This ensures consistency even if commands were
// previously modified manually via @BotFather.
// 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).
@@ -102,40 +95,33 @@ func gatherCommands[T any](bot *Bot[T]) []tgapi.BotCommand {
// 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
}
// Clear existing commands to avoid duplication or stale entries
_, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommands{})
if err != nil {
return fmt.Errorf("failed to delete existing commands: %w", err)
}
// Register commands for each scope
scopes := []*tgapi.BotCommandScope{
scopes := []tgapi.BotCommandScope{
{Type: tgapi.BotCommandScopePrivateType},
{Type: tgapi.BotCommandScopeGroupType},
{Type: tgapi.BotCommandScopeAllChatAdministratorsType},
}
for _, scope := range scopes {
_, err = bot.api.SetMyCommands(tgapi.SetMyCommands{
Commands: commands,
Scope: scope,
})
if err != nil {
return fmt.Errorf("failed to set commands for scope %q: %w", scope.Type, err)
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. It first deletes any existing commands in that scope
// to ensure a clean state, then sets the new set of commands.
// 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.
@@ -150,22 +136,30 @@ func (bot *Bot[T]) AutoGenerateCommands() error {
// 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)
}
_, err := bot.api.DeleteMyCommands(tgapi.DeleteMyCommands{Scope: scope})
if err != nil {
return fmt.Errorf("failed to delete existing commands: %w", err)
func (bot *Bot[T]) setCommandsForScope(ctx context.Context, scope *tgapi.BotCommandScope, commands []tgapi.BotCommand) error {
if len(commands) > 100 {
return ErrTooManyCommands
}
_, err = bot.api.SetMyCommands(tgapi.SetMyCommands{
Commands: commands,
Scope: scope,
})
_, err := bot.api.SetMyCommandsWithContext(ctx, tgapi.SetMyCommands{Scope: scope, Commands: commands})
if err != nil {
return fmt.Errorf("failed to set commands for scope %q: %w", scope.Type, err)
scopeType := tgapi.BotCommandScopeDefaultType
if scope != nil {
scopeType = scope.Type
}
return fmt.Errorf("failed to set commands for scope %q: %w", scopeType, err)
}
return nil
}