In the last post I briefly mentioned blast-news, the bot that sends me a news digest every morning. Since then it has been rewritten a few times, survived a couple of nasty bugs, and one login mystery that had me suspecting Telegram, Docker, and myself for a while. Time to give it its own post.

Where it came from
I'm subscribed to more Telegram channels than I can physically read. The Radio-T chat, Habr, a couple of AI channels, channels about Cyprus where I live, DTF, a group chat with friends. Plus Hacker News and Lobste.rs, which I'd open "for five minutes" and close an hour later.
Reading all of that every day is impossible. Not reading it doesn't work either, FOMO is still a thing. The compromise: let someone read it for me and tell me what mattered. That someone became a bot.
How it's put together
Two containers on the same Raspberry Pi.
The first is the bot itself, in Python. It logs into Telegram as a regular user via Telethon (not as a bot, because bots aren't allowed to read other people's channels), grabs whatever is unread, hits the web sources, and sends the result to my DMs through the normal Bot API.
The second is a tiny Flask service with Claude Code inside. Yes, the coding CLI. I run it headless: claude -p, prompt on stdin, answer on stdout. No API key, it's the same subscription the chat bot from the previous post uses. Simpler for me, and noticeably cheaper.
Every morning at 10:01 Cyprus time the bot:
- walks through 15 channels and collects everything unread from the last day
- pulls the Hacker News front page, Lobste.rs, and the TLDR Tech newsletter
- asks Claude for a summary of each source separately
- sends me a series of messages, one per source
- finally asks Claude to write a "summary of the day" across all of them
One message looks roughly like this:
🟠 Hacker News
AI/LLM:
▸ Title in Russian, one sentence on what it's about — link
▸ Another one — link
Hardware/DIY:
▸ ...
Boring, but it reads in a minute and every item has a link if I want the details.
Unread, not "last 24 hours"
The first version took everything from the last day. The problem: if the digest crashed halfway (and it did), half of that day's news was gone for good. Now the bot relies on Telegram's own unread counter: it takes everything after the last read message and marks the channel as read only once the summary has actually reached me. If it crashed, tomorrow it catches up on yesterday too.
Side effect: if I open a channel and scroll through it myself, it won't show up in the digest. Fine by me: read is read.
When there are too many messages
The Radio-T chat can produce a thousand messages a day. They fit into the model, but it's slow, expensive, and the summary doesn't get any better. The first version solved this the simple way: messages[:600] and text[:40000]. In other words, it cut off the end. And the end is the freshest news. Brilliant.
Now each message gets a rough "weight": reactions, forwards, replies, views, and text length. If there are more messages than the limit, the lightest ones get dropped, which is usually one-word flood and memes. What's left is put back in chronological order, because without it the summary comes out choppy.
Plus deduplication: reposts of the same text within a channel collapse into one, and links from web sources are remembered for a week. The same story from Hacker News and Lobste.rs no longer arrives twice.
A prompt per source
The most hands-on part. One generic prompt for everything produced the same grey mush, so each channel got its own:
- DeepState is grouped by front direction, and if nothing changed, the answer must be empty
- the Cyprus channel is told the reader is planning naturalisation, so anything about residency and citizenship goes first
- Habr, HN, and Lobste.rs get a "reader profile": AI, Python, Raspberry Pi, Flipper, mechanical keyboards, soldering. Not as a filter, just so those topics rank higher
There are shared rules too: plain text only, no Markdown (Telegram breaks it in the most unexpected places), translate everything into Russian, a link on every item, and if someone in the messages recommends a book or a show, put it in a separate "Recommendations" block. That last one I added after missing a couple of good tips in the friends' chat.
Searching the archive
A side feature I use more often than I expected. /search radio_t_chat mechanical keyboards 6m scrolls through six months of the channel, finds the messages that contain all the words from the query, and hands them to Claude to assemble a coherent answer.
The first version searched with "or": any message with the word "mechanical" or the word "keyboards". Everything and anything made it into the results and pushed out the useful stuff. Switched to "and", and it's been fine since.
There's also /news to get the digest right now and /digest <channel> for a single source.
Things that bit me
A few stories so you don't repeat them.
E2BIG. The digest prompt was originally passed as a command-line argument. Linux caps a single argument at 128 KB, and Cyrillic in UTF-8 is two bytes per letter. One busy day on Habr, and instead of a summary I'd get Argument list too long. The prompt goes through stdin now.
The morning "Not logged in". The nastiest one. Every few days both bots would stop responding at the same time with "Please run /login". I'd SSH into the Pi, log in, everything works. A few days later, again.
Here's what it turned out to be. The chat bot, the news bot, and my own terminal sessions share one ~/.claude directory, and therefore one refresh token. The token lives for eight hours. The morning digest is a burst of almost twenty requests in a row, and if it landed exactly on the token's expiry, two processes would try to refresh it at the same time. Whoever got there first won. The second one got invalid_grant, after which the CLI dutifully wiped the credentials. For everyone.
The fix is a file lock: one process does the refresh, the others wait a couple of seconds for the token to become fresh and carry on. While I was at it, I added a Telegram alert for when Claude stops responding because of the login, so I find out from a notification rather than from a missing digest.
4096 characters. Telegram rejects messages longer than that, and long summaries were silently lost with an error in the log. Now the text is split on paragraph boundaries, and if the HTML markup breaks at a seam, the chunk is sent again without markup.
Where it is now
The other day I upgraded everything to Python 3.14, moved the bot to Opus 5.5, and wrote tests. 100% coverage, which sounds ridiculous for a pet project, but I've stopped being afraid to touch it.
Was it worth it? Yes. In the morning I read one long message instead of forty chats and know I haven't missed anything important, important to me specifically. And I open the chats themselves when I feel like it, not because I "need to check".
The code, like everything here, is vibe-coded with manual finishing. If you're curious how the token lock or the message weighting actually works, ask in the comments and I'll write it up.
ava-code
Comments
No comments yet. Be the first.
Sign in or register to leave a comment.