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

InputResult
File — PDF, DOCX, audio (.mp3 / .m4a / .ogg / .opus / .flac), video (.mp4 / .mov / .mkv), images (.png / .jpg), Markdown / text / source codeSummarized 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 URLCaptions if the video has them, audio + Whisper if it doesn’t. Citations become t=SECONDS deep links.
Forwarded Telegram messageAnalyzes 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> linkPulls the chat and analyzes. Requires a Telegram user session installed via /upload_session — the bot token alone can’t read user chats.
@channel refSame 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 .mp4 or YouTube URL, get the talk’s main points without watching.
  • Recorded meeting — .mp4 from 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:

  1. Sends an inline TL;DR as a text message (the report’s first section, lifted out for at-a-glance reading).
  2. Attaches the full report: a PDF by default (/format md sends the Markdown file, /format rich puts the report in the chat itself), with a one-line caption:
    ✓ 23.4s | 1842↓ + 612↑ tok (1280 cached) | $0.0041
    Format: ✓ <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_summary preset: 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 video preset: 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

CommandEffect
/helpShow the input list + this command list.
/pingHealth check — reply pong.
/settingsShow 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.
/stopCancel the run in progress in this chat. Only yours — another admin’s run is untouched.
/confirm on|offToggle the pre-run confirm panel (default: on).
/upload_sessionInstall your Telegram user session (one-time). The bot waits for you to send ~/.unread/storage/session.sqlite.session as a Telegram document.
/cancelAbort 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 from UNREAD_BOT_OWNER_ID stay 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-depth sender_id check 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:

SurfacePrimary ownerExtra 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-luna on 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.