Wire a new Telegram command
The Telegram bot is wired in four layers, all in server/src/interfaces/telegram/ and
server/src/app/i18n/. Follow these steps in order.
-
Declare the command variant.
Open
commands.rsand add a variant to theCommandenum (derived withteloxide::utils::command::BotCommands):#[command(description = "One-line help text shown by /help")]MyCommand(String), // String = the raw argument string; use () for no argsThe
rename_rule = "lowercase"attribute on the enum meansMyCommandregisters as/mycommand. -
Add a route variant to
LinkedCommandRoute.Immediately below the
Commandenum there is a privateLinkedCommandRouteenum. Add a matching arm:MyCommand { args: &'a str },Then wire it in
linked_command_route:Command::MyCommand(args) => Some(LinkedCommandRoute::MyCommand { args }), -
Dispatch to a handler.
In the
match linked_command_route(...)block insidehandle_command, add an arm that calls your handler:LinkedCommandRoute::MyCommand { args } => {super::commands_workspace_handlers::handle_my_command(&bot, chat_id, &state, user_id, locale, args,).await?}Place the handler itself in the appropriate dispatch module:
- Simple read/action commands go in
commands_dispatch.rs. - Workspace-scoped or feature-listing commands go in
commands_workspace_handlers.rs.
A typical handler delegates straight to a service function and uses the shared
send_service_resulthelper:pub(super) async fn handle_my_command(bot: &Bot,chat_id: ChatId,state: &AppState,user_id: Uuid,locale: Locale,args: &str,) -> ResponseResult<()> {send_service_result(bot,chat_id,locale,crate::chat::command::my_command(state, user_id, locale, args).await,).await}send_service_resultsends theResponsePlantext (plain or HTML) on success and falls back to theTelegramCommandFailedmessage on error, so the handler never needs to format errors itself. - Simple read/action commands go in
-
Add Fluent message keys.
Every user-visible string must go through the typed Fluent catalog — no hardcoded strings in handlers.
a. Register the key in
server/src/app/i18n/keys.rs:pub enum MessageKey {// ...existing keys...TelegramMyCommandResult,TelegramMyCommandUsage,}Add the same variants to the
ALLslice and to theidmatch arm using kebab-case:Self::TelegramMyCommandResult => "telegram-my-command-result",Self::TelegramMyCommandUsage => "telegram-my-command-usage",The unit test
message_key_ids_are_unique_kebab_case_entriesinkeys.rswill fail if an id is duplicated or uses a non-kebab character.b. Write the message in both locale files:
server/locales/en/telegram.ftl:telegram-my-command-result = Result: { $value }telegram-my-command-usage = Usage: /mycommand <argument>server/locales/de/telegram.ftl:telegram-my-command-result = Ergebnis: { $value }telegram-my-command-usage = Nutzung: /mycommand <argument>c. Use the key in your service function:
// No dynamic args — plain text:i18n::text(locale, MessageKey::TelegramMyCommandResult)// With substitution variables:i18n::with_args(locale, MessageKey::TelegramMyCommandResult,i18n::arg("value", some_string))If the same formatted string is reused in several places, add a typed helper to
server/src/app/i18n/format.rs(see the existing helpers such astelegram_intervention_sentas a model). -
Verify.
Terminal window cd servercargo checkcargo test archcargo test -- i18ncargo test -- i18nruns the catalog validation test (fluent_catalogs_contain_all_typed_keys) which checks that everyMessageKeyvariant has a corresponding entry in both locale files.
See also
Section titled “See also”server/src/interfaces/telegram/commands.rs—Commandenum andhandle_commanddispatcherserver/src/app/i18n/keys.rs—MessageKeyregistryserver/locales/en/telegram.ftl— English Fluent catalog