Expand description
cratefield-adapter-telegram: the TelegramBot port over the Telegram
Bot API (issue #764), the harness’s chat side. Uses the runtime’s
HttpClient port — no vendor SDK, no
reqwest — so the same adapter runs on Workers and natively, and the
Clock port to drive its local rate budgets so
tests move time by hand.
The outbound half (HttpTelegramBot) speaks the four calls a module
needs — send, edit, delete, answer-callback — and refuses locally what
Telegram would refuse remotely: an exhausted rate bucket returns
TelegramError::RateLimited before any HTTP request is sent. The
inbound half (parse_update, webhook::parse_verified) turns one
webhook delivery into a typed Update, verified before it is parsed.
§cratefield-adapter-telegram
The TelegramBot port over the Telegram Bot API for the Cratefield harness
(issue #764): sendMessage, editMessageText, deleteMessage and
answerCallbackQuery, plus the typed inbound update and the
secret-token webhook — all through the runtime’s HttpClient and Clock
ports. No vendor SDK, no reqwest, so the same adapter runs unchanged on
Cloudflare Workers and natively.
use std::sync::Arc;
use cratefield_adapter_telegram::{HttpTelegramBot, InlineButton, OutgoingMessage};
let bot = HttpTelegramBot::new(Arc::new(fetch), Arc::new(clock), "123456:ABC-DEF…");
let sent = bot
.send_message(
OutgoingMessage::new(chat_id, "Deployment finished")
.row(vec![InlineButton::callback("Acknowledge", "deploy:ack")?]),
)
.await?;
// sent.message_id — the id later edit_message and delete_message calls address.§Local rate budgets, per instance
HttpTelegramBot keeps token buckets in a mutex inside the client: a
global one (30 calls/second), one per private chat (1/second) and one per
group (20/minute, for every chat_id below zero), all fed by the injected
Clock so tests move time by hand. When a bucket is empty the call is
refused with TelegramError::RateLimited before any HTTP request is
sent, so a chatty module cannot spend the bot’s real budget. A 429
from Telegram names a retry_after; the client records a not-before
deadline for that chat and refuses further sends there locally until it
passes.
The buckets are per client instance — in wasm terms, per isolate.
Telegram’s budget is per bot across everything, so a deployment running
several isolates shares nothing: gate the fan-out through the runtime’s
RateLimiter port instead, or accept that each isolate budgets its own
share.
§The bot token never rides an error
Telegram authenticates the bot by the URL path — POST {base}/bot{token}/{method}
— so the token sits in a place most HTTP clients happily quote in an error.
Every string this crate puts into a TelegramError, a Display, a Debug
or a log line is scrubbed of the token first, and the client’s own Debug
prints a placeholder for it. Errors carry Telegram’s description text,
never the request that produced it.
§The inbound half
parse_update reads one webhook delivery into a typed Update: messages,
/command[@bot] args, inline-keyboard callback queries and channel posts;
anything else (edited_message, shipping events, …) is UpdateKind::Other.
Entity offsets are UTF-16 code units, exactly as Telegram counts them, so a
link sliced out of a text that opens with an emoji lands on the right
characters.
webhook::parse_verified checks the X-Telegram-Bot-Api-Secret-Token
header against the secret before the body is parsed — verify first,
parse second, the same order every inbound crate here keeps. Telegram’s
secret alphabet is 1–256 characters of A-Z a-z 0-9 _ -; is_valid_secret
mirrors it for configuration checks. A shared token proves the sender but
nothing stops a replay: claim each update_id through a dedup ledger
(cratefield_core::Inbox) before acting on it.
§Shape of the mapping
Statuses map to TelegramError: 429 → RateLimited (parameters.retry_after
from the body, else the Retry-After header through core’s parser, else one
second), 401 → Unauthorized, 403 → Forbidden with Telegram’s
description, any other 4xx (including 400 and 404) → Invalid, and
5xx, transport failures and unparseable responses → Transient.
InlineButton::callback refuses locally what Telegram would refuse on the
wire: callback_data outside 1..=64 bytes. Message text is sent with
link_preview_options.is_disabled set only when the caller disabled the
preview, so the payload stays minimal.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Modules§
- conformance
- The adapter’s own conformance walk: one pass over the whole
TelegramBotport any implementation must survive. The crate runs it against the fake and the HTTP client in its tests; a module bringing its own bot (a wrapper, a decorator) runs it against that. - fake
- A
TelegramBotthat talks to no one: the fake module tests substitute forHttpTelegramBot, always compiled. It records every call, hands out increasing message ids per chat like Telegram would, refuses edits and deletes of messages that are not there, and can be told to fail exactly once. - webhook
- Telegram’s inbound webhooks: verify the delivery, then parse it. The
inbound sibling of
HttpTelegramBot, which sends.
Structs§
- Callback
Query - An inline-keyboard press: the
dataanInlineButtoncarried, plus who pressed it and where. - Channel
Post - A post in a channel the bot is in.
- Chat
- The chat a message arrived in.
- Command
- A message whose text starts with a
/command. - Http
Telegram Bot TelegramBotover the Telegram Bot API:POST {base}/bot{token}/{method}with a JSON body, through the runtime’sHttpClientport.- Inline
Button - One button in an inline keyboard row.
- Message
- A plain message.
textis the message’stext, or itscaptionwhen the message carries media instead;linksgathers the URLs from both theentitiesand thecaption_entities. - Outgoing
Message - A message to send: text, optionally an inline keyboard, optionally with the link preview suppressed.
- Rate
Limits - The local rate budgets
HttpTelegramBotenforces before it sends, as token buckets fed by the injectedClock. - Sent
Message - A message Telegram accepted: the chat it landed in and the id every
later
edit_message/delete_messagecall addresses. - Update
- One delivery: its id — the dedup key against Telegram’s at-least-once redelivery — and what it carries.
- User
- A Telegram user, as the update carries them.
Enums§
- Button
Kind - What pressing an
InlineButtondoes. - Chat
Kind - What kind of chat a message arrived in.
- Telegram
Error - Why a call to Telegram failed.
- Update
Error - Why a body could not be read.
- Update
Kind - What one delivery carries.
Traits§
- Telegram
Bot - The outbound chat port: the four Bot API calls a module needs. Object
safe, so a module holds an
Arc<dyn TelegramBot>and tests substitutefake::FakeTelegramBot.
Functions§
- parse_
update - Parses one delivery body into an
Update.