Skip to main content

Crate cratefield_adapter_telegram

Crate cratefield_adapter_telegram 

Source
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 on crates.io cratefield-adapter-telegram documentation MIT

§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 TelegramBot port 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 TelegramBot that talks to no one: the fake module tests substitute for HttpTelegramBot, 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§

CallbackQuery
An inline-keyboard press: the data an InlineButton carried, plus who pressed it and where.
ChannelPost
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.
HttpTelegramBot
TelegramBot over the Telegram Bot API: POST {base}/bot{token}/{method} with a JSON body, through the runtime’s HttpClient port.
InlineButton
One button in an inline keyboard row.
Message
A plain message. text is the message’s text, or its caption when the message carries media instead; links gathers the URLs from both the entities and the caption_entities.
OutgoingMessage
A message to send: text, optionally an inline keyboard, optionally with the link preview suppressed.
RateLimits
The local rate budgets HttpTelegramBot enforces before it sends, as token buckets fed by the injected Clock.
SentMessage
A message Telegram accepted: the chat it landed in and the id every later edit_message/delete_message call 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§

ButtonKind
What pressing an InlineButton does.
ChatKind
What kind of chat a message arrived in.
TelegramError
Why a call to Telegram failed.
UpdateError
Why a body could not be read.
UpdateKind
What one delivery carries.

Traits§

TelegramBot
The outbound chat port: the four Bot API calls a module needs. Object safe, so a module holds an Arc<dyn TelegramBot> and tests substitute fake::FakeTelegramBot.

Functions§

parse_update
Parses one delivery body into an Update.