Telegram bot
← Back to README
unread bot run is the same analysis pipeline as the CLI, exposed as
a self-hosted Telegram bot. You message your bot with something; the
bot replies with a report. It answers only the Telegram ids you allow
(by default just yours); everyone else is silently dropped.
This page covers what the bot does. To get one running, one command on a server does it:
curl -fsSL https://raw.githubusercontent.com/maxbolgarin/unread/main/scripts/install-bot-docker.sh | bash
Every other way to run it (Compose by hand, systemd, your laptop) is in
bot-vm-deploy.md.
What you can send
| Input | Result |
|---|---|
File — PDF, DOCX, audio (.mp3 / .m4a / .ogg / .opus / .flac), video (.mp4 / .mov / .mkv), images (.png / .jpg), Markdown / text / source code | Summarized via the same unread <path> pipeline. Audio + video go through Whisper, images through vision. |
| Web URL (any HTTP/HTTPS link) | Page is fetched + extracted (trafilatura), summarized with the website preset. |
| YouTube URL | Captions if the video has them, audio + Whisper if it doesn’t. Citations become t=SECONDS deep links. |
| Forwarded Telegram message | Analyzes the forwarded content; if forwarded from a channel, offers a picker to also pull a day/week/month of that channel. |
t.me/<chat>/<msg> link | Pulls the chat and analyzes. Requires a Telegram user session installed via /upload_session — the bot token alone can’t read user chats. |
@channel ref | Same as the t.me/ form. Needs /upload_session. |
Common use cases
A few of the moments where forwarding to the bot beats anything else:
- Long voice message — a friend sends a 12-minute voice. Forward it; the bot replies with a TL;DR in roughly the time it takes to put your phone down. Or tap 💬 As text to read it instead of listening.
- Podcast / lecture video — drop a
.mp4or YouTube URL, get the talk’s main points without watching. - Recorded meeting —
.mp4from Zoom / Meet / Teams. The audio track is extracted, transcribed, summarized. - Suspicious link — that “you have to see this” URL from a stranger. Forward it; the bot fetches, summarizes, tells you what it actually says without you clicking.
- PDF you’d rather not read — contract, paper, manual. Same drop-in.
- Channel preview — paste a
t.me/<channel>/<msg>link and the bot summarizes the channel’s last day / week / month before you decide whether to subscribe.
Reply format
For each input, the bot:
- Sends an inline TL;DR as a text message (the report’s first section, lifted out for at-a-glance reading).
- Attaches the full report: a PDF by default (
/format mdsends the Markdown file,/format richputs the report in the chat itself), with a one-line caption:
Format:✓ 23.4s | 1842↓ + 612↑ tok (1280 cached) | $0.0041✓ <elapsed>s | <prompt>↓ + <completion>↑ tok [(<cached>)] | $<cost>. Cached-token count is shown only if the provider returned one; cost line is dropped when it’s zero (e.g. a fully cache-hit re-run).
Citations in the report follow the same shape as the CLI: [#42](t.me/…) for Telegram, [#754](youtube.com/watch?v=…&t=754s) for YouTube, [#7](file://…) for local files, paragraph indices for web pages. See sources.md for the full citation matrix.
The confirm panel
When the bot receives something to analyze, it doesn’t run immediately — it sends a small inline panel with a ▶ Run button:
🌐 Web page: https://example.com/article
Preset: `website`
[▶ Run]
Tap Run and the analysis starts. The panel is there so an accidentally-tapped Telegram link doesn’t silently spend money. Per-run tuning happens through slash commands (/preset, /lang, /enrich, /window) and is sticky — set once, applies to every subsequent run in the same chat.
YouTube links get four buttons, because “what’s in it, so I don’t have to watch”, “what does the speaker argue”, “give me the words” and “is any of this true?” are different jobs with very different costs:
🎬 YouTube: https://youtu.be/dQw4w9WgXcQ
What do you want?
[📄 Summary] [▶ Analyze]
[📝 Transcript] [🔎 Fact-check]
- 📄 Summary — the
video_summarypreset: what the video is about, retold in order with a timestamp for each part, the takeaways, and whether it’s worth watching anyway. For when you don’t want to watch. - ▶ Analyze — the
videopreset: the speaker’s position, their main claims with the arguments behind each, and how well the argument holds together (without checking facts — that’s Fact-check). Report as PDF/.md. - 📝 Transcript — no analysis at all. The bot writes
transcript.md(metadata header + plain transcript text) and uploads it as a Markdown file. No LLM call, so the only possible cost is Whisper, and only when the video has no usable captions. - 🔎 Fact-check — pulls the checkable claims out of the video and verifies them against the web, then replies with a verdict table plus per-claim detail and source links. The most expensive of the three: flagship model and a per-search fee. See Fact-checking for how it behaves on providers without web search, and why the cost caption understates this one.
Voice, video messages and audio/video files get the same idea, minus fact-check:
🎙 Voice message
What do you want?
[▶ Analyze]
[📝 Transcript] [💬 As text]
- ▶ Analyze — transcribe with Whisper, then summarize, like any other file.
- 📝 Transcript — no analysis: the transcript uploaded as
transcript.md, with the usual time/cost caption. - 💬 As text — no analysis, and nothing but the words: the transcript comes back as ordinary chat messages (split at Telegram’s message limit), with no header and no stats line, ready to read, copy or forward.
A voice or video forwarded from a channel shows the same three buttons above the channel options.
Fact-check isn’t YouTube-only — /preset factcheck makes it the default for everything you send in that chat, articles and forwarded posts included.
Forwarded messages get a richer picker. If you forward a message from a channel, the panel asks what to analyze:
- This message / image / caption — just the forwarded content
- From this msg in channel — open the source channel from the forwarded message as the start anchor
- Channel · day / week / month — pull a time-window of the source channel and summarize
To skip the panel, send /confirm off once. The bot will then run analyses immediately on receipt (the default before this gate was added). /confirm on puts it back.
Slash commands
| Command | Effect |
|---|---|
/help | Show the input list + this command list. |
/ping | Health check — reply pong. |
/settings | Show this chat’s settings with a button for each: language, format, preset, period, media, confirm, plus the bot-wide AI provider, model and API key. Every slash command below can also be changed from there. |
/preset <name> | Sticky preset for this chat (e.g. /preset digest). Bare /preset clears the override. Names match the CLI: summary, tldr, digest, highlights, quotes, links, action_items, decisions, questions, reactions, factcheck, video, video_summary, website. |
/lang <code> | Sticky report language (e.g. /lang en, /lang ru). Bare clears. |
/enrich <list|all|none> | Sticky extra enrichments for Telegram chat analyses. /enrich image,link turns those two on; /enrich all enables every kind; /enrich none strips even the defaults. |
/window <day|week|month|msg|from_msg|none> | Sticky default time window for TG-chat analyses. |
/format <pdf|md|rich> | How reports come back. pdf — rendered document, best on phones (default). md — the raw Markdown file. rich — the report in the chat itself, nothing to download. Telegram renders the Markdown natively (tables, headings, lists) in one message of up to 32768 characters, using the rich messages added in Bot API 10.1. Where that isn’t available — an older server, or a client that can’t draw it — the report is flattened to the bold/italic/link subset Telethon can send and split at the server’s message limit, never mid-heading. Bare /format restores the default. |
/stop | Cancel the run in progress in this chat. Only yours — another admin’s run is untouched. |
/confirm on|off | Toggle the pre-run confirm panel (default: on). |
/upload_session | Install your Telegram user session (one-time). The bot waits for you to send ~/.unread/storage/session.sqlite.session as a Telegram document. |
/cancel | Abort a pending prompt: /upload_session, or a model id / API key the bot is waiting for. |
Sticky settings are per chat and persistent — stored in data.sqlite::bot_chat_settings and restored at startup, so a docker compose up doesn’t reset them. Each admin has their own 1:1 chat with the bot, so each admin has their own settings.
Language
/lang is the one to know about in a multi-admin bot:
you: /lang ru → your analyses, report headings and transcripts in Russian
admin #2: /lang en → theirs in English
It sets everything language-related for that admin: the language the LLM writes in, the report’s own ## Sources / ## Verification headings and metadata table, and which caption track a YouTube transcript prefers. /lang none clears it back to the bot’s config default.
Transcripts fall back rather than fail: ask for English on a video with no English captions and you get the captions that exist, with a ⚠️ line at the top of the transcript saying which language it actually is. The analysis is still written in your language — only the source text falls back.
Transcripts are cached per requested language, so you asking for Russian and admin #2 asking for English don’t evict each other, and neither pays to re-transcribe what the other already fetched.
Who can use the bot
The bot answers an explicit allowlist of Telegram IDs — by default just you.
UNREAD_BOT_OWNER_ID=111222333 # solo
UNREAD_BOT_OWNER_ID=111222333,444555666 # you + one more admin
You don’t have to edit it by hand: the installers ask “Anyone else?” after your own id, and later
unread-bot users add 444555666 # Docker install; restarts the bot
unread-bot users remove 444555666
unread bot users add 444555666 # `unread bot` install
People find their id by messaging @userinfobot.
- At startup, the bot probes
~/.unread/storage/session.sqlite.session(the user session). If an authorized session is there, its account becomes the primary owner. Any ids fromUNREAD_BOT_OWNER_IDstay on as extra admins. - If there’s no session AND no
UNREAD_BOT_OWNER_ID, the bot refuses to start handling events — there’s no safe allowlist. - After a successful
/upload_session, the primary owner is re-derived from the just-installed session; the previous primary stays on as an admin. - Every event is filtered by Telethon’s
from_users=AND a defense-in-depthsender_idcheck inside the handler. Anything else is silently dropped — no acknowledgement, no log noise to the sender.
Primary owner vs extra admins
Every admin shares one Telegram user session — the primary owner’s. So a t.me/... link sent by an extra admin would read the primary owner’s chats. Three surfaces are therefore primary-only:
| Surface | Primary owner | Extra admins |
|---|---|---|
| Files, voice, web links, YouTube | ✅ | ✅ |
t.me/... links (chats & channels) | ✅ | ❌ refused |
| Forwarded msg → analyze the source channel | ✅ | ❌ refused (analyzing the forward itself is fine) |
/upload_session | ✅ | ❌ refused |
Extra admins get their own sticky settings (/preset, /lang, …) — _chat_state is keyed by chat.
Add someone only if you’re fine with them spending your API budget. They can’t read your Telegram, but every analysis they run bills your key.
/upload_session is gated by the bootstrap allowlist so a fresh deploy with UNREAD_BOT_OWNER_ID set (but no session yet) lets only you upload the session; nobody else can install themselves as the bot’s owner.
Changing provider, model and keys from the bot
A container has its own ~/.unread, so unread settings on your laptop
does not reach a deployed bot. /settings gives you an inline menu.
The per-chat rows (🌐 Language, 📄 Format, 🎛 Preset, 📅 Period, 🖼 Media,
✅ Confirm) set the same values as the slash commands, for your chat only.
The AI rows are bot-wide:
- 🔀 Provider — openai / openrouter / anthropic / google / local. Applies immediately, no restart, and is persisted so it survives one.
- 🧠 Model — loads the provider’s own model list with your key (any
provider, including a local server) and shows the popular ones your key
can use: listed with prices per 1M tokens in/out, as buttons, and with
links to the provider’s price and model pages. 🔄 Reload fetches the
list again; send any other id as a message.
default (shown with the model it means, e.g.
gpt-5.6-lunaon OpenAI) clears your pick: runs use the model the presets pin, or the provider’s own default where the pin is another provider’s model. - 🔑 API key — the bot asks for the key as your next message, stores it in the secrets DB, and deletes your message. The confirmation shows only a masked tail.
On the key: deleting the message removes it from the chat history, but it
still travelled through Telegram’s servers to get there. If that’s not
acceptable for your threat model, set the key in the server’s .env
(unread-bot config) instead — the bot never asks for one you’ve
already provided that way.
Only the primary owner can rotate keys; credentials are bot-wide, so a
second admin changing them would silently change everyone’s runs.
Model list and prices stay current on their own
The bot re-downloads the model catalog (names, prices, context windows)
from this repo’s main branch once a day, so a new model or a price
change shows up in 🧠 Model and in cost reports without rebuilding the
image. The last good copy is cached in ~/.unread/storage/, so a restart
doesn’t need the network, and a failed or malformed download keeps the
catalog you already have. Turn it off with
UNREAD_AI_CATALOG_REFRESH_HOURS=0, or point UNREAD_AI_CATALOG_URL at
your own copy. Prices you set in [pricing] always win.
Privacy & data flow
The bot machine holds the same ~/.unread/ directory the CLI would:
SQLite cache, generated reports, your API keys, your Telegram user
session (after /upload_session). API calls go to your chosen
provider (OpenAI, Anthropic, Gemini, OpenRouter, or a local
OpenAI-compatible endpoint). The only network endpoints are Telegram
servers, your AI provider, and any URLs you point the bot at.
If you self-host on a VM, treat the disk like the CLI’s disk: snapshot
~/.unread/storage/, encrypt at rest, restrict access. See
security.md for the credential-storage options
(keystore, passphrase-derived pass) the CLI also supports — they
all work for the bot.
Running it
One command on a fresh server installs Docker if needed, asks for the bot token, your AI key and Telegram API id/hash, finds your Telegram id and starts the bot:
curl -fsSL https://raw.githubusercontent.com/maxbolgarin/unread/main/scripts/install-bot-docker.sh | bash
Then unread-bot status | logs | update | config | users manages it.
Already use the CLI? Run the bot from it, no Docker needed — it becomes a systemd user service where the machine has one, a background process otherwise:
unread bot setup && unread bot start
Compose by hand, all the unread bot commands, and adding a Telegram
session are in bot-vm-deploy.md.