✅ fresh
last synced 2026-10-24T20:09:49.281395+00:00 · coverage 100% (
telegram-notifier)Validation by Beadloom
doc_sync— same source assync-check.
Telegram notifier
The Notifier port over the Telegram Bot API: a photo with a caption or a text message. A failed delivery is logged and never raised, and the token never reaches a log.
Source
src/horizon_tracker/infrastructure/telegram.py
Specification
Purpose
Every run sends one message to the configured chat (TELEGRAM_CHAT_ID) from the Telegram bot whose token the service holds (TELEGRAM_BOT_TOKEN). A report that carries a screenshot, of a confirmed success or of a failure, goes out as that photo with the text as its caption. A run must never fail because its notification failed, and the bot token, which sits in the path of every Bot API URL, must not leak into the journal.
How it works
send(report) renders the text with render(report, language) from the domain, in the language the notifier was built with (the CLI passes Settings.language, read from HT_LANGUAGE, English by default), and then:
- if the report has a screenshot, reads it and posts
sendPhotowith the text as the caption; - if there is no screenshot, the file cannot be read, or
sendPhotofails or is refused, postssendMessagewith the same text.
Each call is a POST to https://api.telegram.org/bot<token>/<method> with a 20-second timeout, bounded so a hung API cannot hold the run. A network error or a status other than 200 is logged with the error's text or the API's description, with the token replaced by <token>, and the call answers False.
The constructor installs a filter on the httpx logger, once per process, that masks the token in any Bot API URL a log record mentions, so httpx's own request lines carry bot<token> instead of the token.
Invariants
send()never raises for a delivery failure.- The module never logs a URL or an unredacted error.
- The screenshot is an attachment; the text never names its path.
- A malformed token (not
<bot id>:<secret>) or an empty chat id is refused at construction withValueError, whose message never echoes the token. The CLI turns that refusal into a configuration error forTELEGRAM_BOT_TOKEN.
API
Module src/horizon_tracker/infrastructure/telegram.py:
TelegramNotifier(token: str, chat_id: str, client: httpx.Client, language: Language)— implementsNotifier;send(report: RunReport) -> None. The language has no default, so no caller gets English by forgetting it.BOT_API_URL—"https://api.telegram.org/bot{token}/{method}".TIMEOUT_SECONDS—20.0.
The token discipline is public because the bot transport (telegram-bot) calls the same API with the same token and imports it from here instead of repeating it:
check_bot_token(token: str) -> None— raisesValueErrorfor a token not shaped<bot id>:<secret>, without echoing it;redact_token(text: str, token: str) -> str— the text with the token masked as<token>;mask_token_in_httpx_log() -> None— installs the masking filter on thehttpxlogger, once per process;bot_api_description(response: httpx.Response) -> str— the API'sdescriptionof a failure, or(no description).
Testing
Tests: tests/unit/infrastructure/test_telegram.py, on httpx.MockTransport.
The acceptance suite runs the real notifier over a recording Bot API (tests/support/bot_api.py), built by the CLI's own build_notifier from the settings the command loaded. Most feature files quote the notification in Russian, so the acceptance scene builds the notifier in ru. The scenarios of language.feature leave the language to HT_LANGUAGE, so their notifier is built the way the service builds it. Scenarios (tests/acceptance/features/notifications.feature):
A failed run sends the reason and a screenshotA notification never contains the password
Scenarios (tests/acceptance/features/screenshot_preview.feature), on a fake Horizon client:
A successful check-in sends a screenshot of the opened desktopA successful check-out sends a screenshot of the launch list without a sessionA check-in whose screenshot failed still reports success as text
Scenarios (tests/acceptance/features/language.feature):
A notification is rendered in the configured languageThe default language is EnglishA blank HT_LANGUAGE reads as the default English
Dependencies
Derived from the imports on reindex.
- Depends on:
messages,run-report - Used by:
cli,telegram-bot - External contracts (declared in
.beadloom/_graph/landscape.yml, not derived): consumeshttp:telegram-sendMessageandhttp:telegram-sendPhotofromtelegram-bot-api, and producesnotification:run-reportforowner-chat
Parent
infrastructure — infrastructure README