Skip to main content

webserver_base/telegram/
mod.rs

1//! An outbound Telegram Bot API notifier.
2//!
3//! This module exists to replace the hand-rolled `sendMessage` calls which had
4//! been copied between projects, each with a different subset of the hard parts
5//! missing. It is send-only on purpose: it has no polling, no webhooks, and no
6//! update handling, because none of the consuming projects receive anything.
7//!
8//! # Formatting without escaping
9//!
10//! Messages are built from [`Message::builder`], which emits Telegram
11//! *entities* rather than `parse_mode` markup. Telegram's own documentation
12//! describes entities as what a Markdown or HTML parser is converted *into*, so
13//! nothing is lost by skipping that step — and because no markup is ever
14//! embedded in the text, interpolated values never need escaping:
15//!
16//! ```no_run
17//! use webserver_base::telegram::{ChatId, Message, ReqwestTelegram, Telegram, TelegramSettings};
18//!
19//! # async fn example(untrusted_filename: &str) -> Result<(), Box<dyn std::error::Error>> {
20//! let settings = TelegramSettings::builder("123456789:AA...").build()?;
21//! let telegram = ReqwestTelegram::new(settings, None)?;
22//!
23//! telegram.send(
24//!     ChatId::Id(1234),
25//!     Message::builder()
26//!         .text("🎨 ")
27//!         .bold("New pattern")
28//!         .text("\nInput: ")
29//!         .code(untrusted_filename) // no escaping, ever
30//!         .build(),
31//! );
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! # What it handles
37//!
38//! - **Length.** Telegram caps text at 4096 and captions at 1024 UTF-16 code
39//!   units. Oversized messages are split at natural boundaries, with entities
40//!   clamped and rebased onto each piece, capped at a configurable number of
41//!   chunks so an upstream bug cannot become a flood.
42//! - **Rate limits.** Sends are queued per chat and paced at Telegram's
43//!   documented limits: one message per second per chat, thirty per second
44//!   overall.
45//! - **Retries.** A `429` is retried after the `retry_after` Telegram supplies,
46//!   up to a ceiling; transient failures back off exponentially; permanent
47//!   client errors are never retried, and misconfiguration is logged loudly.
48//! - **Secrets.** The bot token is a path segment of every request URL, so it
49//!   is held in a [`BotToken`] which refuses to print itself, and every
50//!   `reqwest` error has its URL stripped before it can reach a log.
51
52mod chat_id;
53mod chunk;
54mod entity;
55mod error;
56mod message;
57mod notifier;
58mod queue;
59mod settings;
60mod token;
61mod utf16;
62
63pub use chat_id::ChatId;
64pub use entity::{Entity, EntityKind, Style};
65pub use error::TelegramError;
66pub use message::{FileSource, InlineBuilder, Media, Message, MessageBuilder, SendOptions};
67pub use notifier::{MockTelegram, ReqwestTelegram, SentMessage, Telegram};
68pub use settings::{
69    DEFAULT_CONNECT_TIMEOUT, DEFAULT_GLOBAL_PER_SECOND, DEFAULT_MAX_CHUNKS,
70    DEFAULT_MAX_INPUT_BYTES, DEFAULT_MAX_RETRIES, DEFAULT_MAX_RETRY_AFTER,
71    DEFAULT_PER_CHAT_INTERVAL, DEFAULT_QUEUE_CAPACITY, DEFAULT_REQUEST_TIMEOUT, MAX_CAPTION_LENGTH,
72    MAX_TEXT_LENGTH, TELEGRAM_API_BASE_URL, TelegramSettings, TelegramSettingsBuilder,
73};
74pub use token::BotToken;