feat(alias): add a shared alias dictionary invocable as a bare command

/alias <name> saves a replied message under a name and /insert <name> sends it
back; /aliases lists every name and /unalias deletes one. Every Telegram format
is supported — sticker, photo, GIF, video, video note, audio, voice, document,
plain text — and each is kept as the file_id Telegram already issued, so nothing
is downloaded and an alias survives redeploys. The namespace is global and the
last assignment wins, matching the shared sticker pack; /unalias is open to
anyone for the same reason.

A saved name also works as its own command: /cheer rather than /insert cheer.
This needs two new seams in the module contract. Module.Fallback handles a
/command no module registered, and the dispatcher installs it after every
Command — the bot library returns the first matching handler, so code always
beats a name resolved at runtime, including an alias that shares a command
added in a later build. /alias refuses a name already in the registry for the
same reason, since such an alias would only reach /insert. An unknown command
stays silent: the fallback sees every unrecognised /foo in every chat, so
replying would make typos noisy and would confirm which names exist.

Module.Inline answers inline-mode queries — "@botname <prefix>" from any chat,
filtered by prefix and capped at Telegram's 50 results. Each result is a cached
inline type carrying the stored file_id, so the picker renders real previews
without an upload. Video notes are omitted because Telegram defines no
InlineQueryResultCachedVideoNote and substituting a plain video would change
what was saved. Inline mode must be enabled in BotFather before Telegram
delivers these updates.

Both slots are single-occupancy with conflict detection at Build. Auth.Permits
learns the inline sender so a gated inline handler would not deny everyone.
Build's command indexing and slot claiming move into addCommands/addSingletons,
keeping it under the project's cyclomatic cap.

Also restores the sticker module: /addsticker moves back out of util, which has
no store, into internal/modules/sticker as its only command.
This commit is contained in:
tiennm99 committed 2026-09-04 11:36:50 +07:00
1 parent 822bcdbca8
commit be91d2eb41
33 files changed
+1882 -52

No files matched your search

+28
View File
@@ -74,6 +74,34 @@ type Module struct {
Callbacks []Callback
Crons []Cron
CommandHook func(ctx context.Context, name string, update *models.Update) // optional; called by dispatcher after each authorized command invocation. update carries the originating Telegram update so hooks can attribute usage to a user.
Fallback *CommandFallback // optional; handles a /command no module registered. At most one across all modules.
Inline *InlineQuery // optional; handles inline-mode queries. At most one across all modules.
}
// CommandFallback handles a /command that no module registered.
//
// Install registers it after every Command, and the bot library returns the
// *first* matching handler, so a registered command can never reach here. That
// ordering is the whole mechanism: code always wins over anything resolved at
// runtime, including an alias that shares a command's name.
//
// Name is the parsed command, lowercased with any @botname suffix stripped —
// the same normalisation matchCommand applies — so a fallback never re-parses
// the entity itself.
type CommandFallback struct {
Visibility Visibility
Handler func(ctx context.Context, b *bot.Bot, name string, update *models.Update) error
}
// InlineQuery handles inline-mode queries ("@botname <text>" typed in any chat).
//
// The bot library has no HandlerType for inline queries, so Install matches
// update.InlineQuery itself. Inline mode must also be enabled for the bot in
// BotFather; without that Telegram never delivers these updates and the handler
// is simply never called.
type InlineQuery struct {
Visibility Visibility
Handler func(ctx context.Context, b *bot.Bot, update *models.Update) error
}
// Deps is the dependency bundle a Factory receives.