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;