News Catcher is a self-hosted Telegram bot that turns news URLs into clean article text in your chat. It supports a domain allowlist, anti-bot fetching (including Playwright for Cloudflare sites), listen-aloud via KittenTTS, saving articles for local experiments, and an optional overnight audio briefing pipeline (RSS + local LLM → Google Drive).
Designed for personal use: you control which publishers are allowed, who can use the bot, and where audio is stored.
Search terms that describe this repo: telegram news bot, article extractor, read later telegram, news text to speech, kittentts, trafilatura, readability, playwright scraping, self-hosted news reader, audio news briefing, python telegram bot.
If you maintain a fork or publish this repo, add Topics such as:
telegram-bot telegram news news-aggregator article-extraction web-scraping playwright text-to-speech tts kittentts python python3 self-hosted readability trafilatura rss ollama google-drive personal-assistant
- URL in -> article text out through Telegram
- Extracts article images and sends each with caption (when available)
- Domain allowlist (approved domains only)
- Optional admin PIN for domain management commands
- Oversize download prompt (Proceed / Cancel) using soft and hard byte limits
- Paragraph-aware chunking for Telegram with
N of Mchunk headers - Clear error messages for common anti-bot blocks (
401,403) - Patchright (stealth Playwright) fallback for Cloudflare-protected domains (default:
economist.com,marktechpost.com) - Startup online notification to allowed Telegram users
- Speak last article: after a URL, send
newscatcher, speak to mefor TTS audio (KittenTTS) - Overnight briefing (optional):
python -m briefing→ Google Drive (briefing.yaml, see.env.example)
Seeded in domains.json as examples (replace or extend for your own outlets):
economist.comreuters.comwashingtonpost.comtheintercept.comnypost.com
You can add/remove domains at runtime with bot commands.
News_bot.py- Telegram handlers, command flow, chunking, and messagingfetch.py- HTTP fetching, redirects, limits, anti-SSRF checks, Playwright fallback triggerfetch_playwright.py- Headless Chromium fetch for challenge-protected pagesextract.py- Article extraction (trafilatura+ readability fallback)domains_store.py- Load/save domain allowlist and host matchingconfig.py- Environment/config loadingdomains.json- Approved domains list.env.example- Example environment configurationarticle_cache.py- Last article per user (for TTS)tts.py- KittenTTS + ffmpeg MP3 generationbriefing/- Overnight multi-source briefing → Google Drivedocs/NewsCatcher_logo.png- Project logo (shown above)docs/future_ideas.md- Roadmap and design notestests/- Unit tests for extraction, chunking, and domain checks
- Python 3.10+ (3.12 recommended)
- Linux/macOS/WSL (works on Linux Mint; Playwright may warn and use Ubuntu fallback build)
- Telegram bot token from BotFather
- Your Telegram numeric user ID
Python dependencies are listed in requirements.txt.
- Create and activate a virtual environment:
python3 -m venv .venv
source .venv/bin/activate- Install dependencies:
pip install -r requirements.txt- Install Patchright browser runtime (required for Economist/MarkTechPost/Cloudflare fallback):
patchright install chromium- Create your
.envfrom.env.exampleand set values:
cp .env.example .envMinimum required values:
TELEGRAM_BOT_TOKENALLOWED_TELEGRAM_USER_IDS(comma-separated numeric IDs)
python News_bot.pyOn startup, the bot sends an "online and ready" message to each allowed user ID.
Send any message containing a URL from an approved domain.
The bot will:
- Validate URL and domain
- Fetch article HTML
- Extract readable text
- Return text in Telegram-safe chunks
- Send extracted article images with captions
Each chunk starts with:
1 of X
- Send a news URL and wait for the text reply.
- Send:
newscatcher, speak to me(case-insensitive). - The bot generates an MP3 with KittenTTS and sends it as Telegram audio.
The bot shows inline buttons: Speak to me and Save to disk. You can tap those or type newscatcher, speak to me / newscatcher, save to disk.
Save to disk writes the last article body to test_articles/ as a .txt file named from the first six words of the title (underscores between words). Override directory with TEST_ARTICLES_DIR in .env.
Run the interactive script (lists the 9 newest files in TEST_ARTICLES_DIR, then prompts for article and voice(s)):
python test_article_to_audio.pyTTS options (model, speed, etc.) are at the top of test_article_to_audio.py. Output MP3s are named like article_stem_Jasper.mp3 next to the .txt.
List voices only: set LIST_VOICES_ONLY = True in that file and run again.
Requires ffmpeg on PATH, espeak-ng (or espeak), TTS_ENABLED=1, and KittenTTS 0.8.1 from requirements.txt (not the older PyPI kittentts 0.1.x package).
pip install -r requirements.txtDefault model: KittenML/kitten-tts-mini-0.8. Voices: Bella, Jasper, Luna, Bruno, Rosie, Hugo, Kiki, Leo (set TTS_VOICE in .env).
KittenTTS cannot disambiguate homographs (e.g. Polish the country vs. nail polish) and often pauses on dotted abbreviations like U.S. because its internal chunker splits on periods. News Catcher applies your rules before synthesis.
- Edit
tts_replacements.jsonin the project root (or point elsewhere withTTS_REPLACEMENTS_FILEin.env). - Keep
TTS_NORMALIZE_ENABLED=1(default). Set to0to pass article text through unchanged. - Restart the bot or re-run
test_article_to_audio.pyafter changing the JSON (rules are cached per process).
File format:
{
"replacements": [
{"from": "U.S.", "to": "United States"},
{"from": "U.K.", "to": "United Kingdom"}
],
"regex": [
{
"pattern": "\\bPolish\\b(?=\\s+government)",
"replace": "Poleish",
"flags": "i"
}
]
}replacements— simple find-and-replace strings, applied in file order (longerfromstrings are applied first automatically soU.S.A.wins overU.S.).regex— optional Python regex rules; use for context (country sense of Polish only before government, army, etc.). Optional"flags": "i"for case-insensitive.
Shipped defaults expand common abbreviations (U.S., U.K., E.U., U.N.) and rewrite Polish before news-y following words to Poleish (a spelling KittenTTS reads as the country). Add your own entries for Turkey, Jordan, Georgia, outlet-specific names, etc.
Try a phrase locally:
from tts_normalize import normalize_for_tts
print(normalize_for_tts("The Polish government and the U.S. envoy met."))Normalization runs on Telegram speak, overnight briefing audio, and test_article_to_audio.py (all use synthesize_to_mp3 in tts.py).
Try spellings in Telegram (writes to tts_replacements.json):
/pronounce Polish Poleish Pole-ish
You get a short audio clip per spelling; tap Save on the one you want. Without audio:
/add_pronunciation Polish Poleish
Article speak runs in the background — you can fetch the next URL while audio is generating.
/start- intro/help/list_domains- show approved domains/add_domain <domain>or/add_domain <PIN> <domain>(ifADMIN_PINis set)/fix_403 <domain>— record a 403-blocked site and retry the last failed URL/pronounce <word> <alt1> [alt2…]— hear pronunciation options and save totts_replacements.json/add_pronunciation <from> <to>— add a rule without generating samples/remove_domain <domain>or/remove_domain <PIN> <domain>(ifADMIN_PINis set)
- Only users in
ALLOWED_TELEGRAM_USER_IDScan use the bot - Optional
ADMIN_PINfor sensitive domain changes - HTTP fetch restricted to approved domains
- Redirects are revalidated against approved domains
- DNS/IP checks block private/loopback/link-local destinations
FETCH_SOFT_MAX_BYTES: normal max before askingFETCH_HARD_MAX_BYTES: absolute ceiling even after approval
If soft limit is exceeded, the bot prompts:
- Proceed - refetch with raised cap (up to hard max)
- Cancel - abort fetch
Some sites (notably Economist) may return HTTP 403 to plain HTTP clients.
On anti-bot HTTP responses (402, 403, configurable), the bot tries fallbacks in order (for any allowlisted domain when AUTO_403_FALLBACKS=1, default):
- WordPress REST API — domains in
WORDPRESS_API_DOMAINS(default:marktechpost.com). - TLS impersonation (
curl_cffi) — works for Cloudflare (Politico) and Le Monde (402). - Headless Chromium (patchright).
If a new site returns 403, send the URL again; bypass runs automatically. If it still fails, use /fix_403 politico.com (or /fix_403 right after the failed URL to retry).
Config:
ANTIBOT_FALLBACK_STATUSES(default:402,403)AUTO_403_FALLBACKS(default:1) — try curl_cffi + browser for all allowlisted domainsWORDPRESS_API_DOMAINS(default when omitted:marktechpost.com)PLAYWRIGHT_FALLBACK_DOMAINS(used whenAUTO_403_FALLBACKS=0)PLAYWRIGHT_TIMEOUT_MS(default:60000)PLAYWRIGHT_CHANNEL(optional; defaults tochromewhengoogle-chromeis installed)CURL_CFFI_IMPERSONATE(default:chrome131)
To disable fallback, set:
PLAYWRIGHT_FALLBACK_DOMAINS=- Copy
briefing.yaml.example→briefing.yamland set RSS feeds. - Create a Google Cloud service account with Drive API access; share your target Drive folder with the service account email.
- Set in
.env:GOOGLE_DRIVE_FOLDER_ID,GOOGLE_APPLICATION_CREDENTIALS,OLLAMA_HOST,OLLAMA_MODEL. - Run locally or via cron:
python -m briefingUploads briefing-YYYY-MM-DD.mp3 (and metadata JSON) to your Drive folder and deletes files older than BRIEFING_RETENTION_DAYS (default 7).
Example cron (3 AM daily):
0 3 * * * cd /path/to/News_Catcher && .venv/bin/python -m briefing >> logs/briefing.log 2>&1Run tests:
PYTHONPATH=. python -m unittest discover -s tests -vIncludes tests for:
- extraction output
- paragraph/chunk formatting
- domain allowlist matching
- Use a browser-like
USER_AGENT(default in.env.example) - Do not append a custom bot token/name to the UA string
- Install Patchright browser:
patchright install chromium
- Ensure the domain is in
PLAYWRIGHT_FALLBACK_DOMAINS(marktechpost.com is included by default)
You may see:
BEWARE: your OS is not officially supported ... downloading fallback build
This is expected on Mint and usually works fine.
If a bot token is ever exposed, revoke and rotate it immediately in BotFather.
Licensed under the GNU General Public License v3.0 (GPL-3.0).
This project is intended to be forked and adapted for your own Telegram user IDs and domain list. Start from Setup, copy .env.example to .env, and adjust domains.json for the news sites you read.
Issues and pull requests are welcome if you improve extraction, TTS quality, or briefing workflows for the community.
