Bot that posts RSS feeds to Telegram. Made mostly for personal use, like an RSS-reader.
rss-to-telegram-engine is designed to work with native RSS feeds and most RSS generators. UX is a priority.
Real examples of rss-to-telegram-engine on Telegram:
- @hema_feed - HEMA and Fencing sports news (youtube, reddit, telegram, vk)
- @photo_feed - Photography and videography (youtube, reddit, telegram)
See Feed Examples to see screenshots of some specific examples.
- Features
- Run
- Config
- Commands
- Processors
- Suggestions
- Feed Examples (Reddit, Youtube, Youtube to mp3, Telegram, Vk)
- Alternatives
- Multi-channel with hierarchical and per-feed configuration
- Media extraction, with yt-dlp integration
- Templates
- Filter by content
- Robust HTML to Telegram markup conversion
- Config is human-readable json
- State is also json
- ...
- Create bot in @BotFather
- Create and fill out .env (in root directory)
- Create and fill out config.json (in config directory)
- Run
- Using uv:
uv run python -m bot.main - Using docker compose:
docker compose up -d --build
- Using uv:
All configuration files, state, templates, cookies, and proxy files should be placed in the config/ directory.
Example .env
# Bot token from @BotFather
BOT_TOKEN=your_bot_token_here
# Comma-separated list of user IDs or channel IDs
ADMIN_IDS=123456789
# Logging level: DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# Path to main configuration file
CONFIG_PATH=config/config.json
# Path to state file
STATE_PATH=config/state.json
# Local Telegram Api (Optional, uncomment container in docker-compose.yml)
# Allows for larger uploads up to 2GB in size
# TELEGRAM_API_SERVER_URL="http://telegram-api-server:8081"
# TELEGRAM_API_ID="123..."
# TELEGRAM_API_HASH="12345..."
Example config.json
{
"_comment": "You can split configs into multiple files using 'includes'.",
"includes": [],
"global": {
"check_interval": 300,
"enable_preview": true,
"processing": {
"media_extract": {},
"html_to_telegram": {},
"jinja_formatter": {}
}
},
"channels": [
{
"id": -1001234567890,
"name": "tech news feed",
"feeds": {
"https://hnrss.org/newest": {
"note": "This feed will use title from RSS since name is null",
"processing": {
"media_extract": {},
"html_to_telegram": {},
"jinja_formatter": {
"show_author": true,
"show_title": true,
"show_content": true
}
}
},
"https://github.blog/feed/": {
"name": "GitHub Blog",
"note": "Custom name overrides RSS title.",
"check_interval": 600,
"processing": {
"media_extract": {
"download_media": true,
"max_media_size": 20971520,
"download_timeout": 30
},
"html_to_telegram": {},
"jinja_formatter": {
"show_author": false
}
}
}
}
}
]
}You can override behavior of some processors for individual feeds using extra_flags. This is passed to all processors and allows per-feed customization without creating separate channel blocks.
"feeds": {
"https://example.com/feed": {
"extra_flags": {
"skip": true,
"append_text": "#important @admin"
}
}
}Supported flags:
skip- Skip this feed entirely bycontent_filterprocessorappend_text- Override text and/or position inappend_textprocessor
Only admins specified in .env can use commands.
/helpor/status- pull up Command list and Bot status/list- list out all live feeds/test- test feed url/reload- reload config after you edited the json files.
Processors transform RSS entries before sending to Telegram. They run in the order specified in processing config. Each processor can be configured per-feed, per-channel, or globally.
Any processor pipeline should end with html_to_telegram and jinja_formatter processors to guarantee a successful telegram message.
Extracts media (images, videos, audio) from entry content and enclosures.
Options:
download_media(bool, default: true) - Download media to buffers when True. Pass as urls when False (sometimes less robust)max_media_size(int, default: 20MB) - Maximum file size in bytesdownload_timeout(int, default: 30) - Download timeout in secondsskip_if_has_media(bool, default: false) - Skip if entry already has media (useful when run after yt-dlp processor)remove_media_tags(bool, default: true) - Remove media tags from content
"media_extract": {
"download_media": true,
"max_media_size": 10485760
}Converts HTML content to Telegram-compatible HTML. Should run after media_extract but before jinja_formatter.
"html_to_telegram": {}Formats the final message using Jinja2 templates. Should be the last processor (or before append_text).
All config options are passed to the template as context variables, so templates can define their own options.
Processor options:
template(string, default: "default") - Template name (without .j2)feed_name- Custom feed name (overrides RSS title)content_use_blockquote(bool) - Wrap content in expandable blockquoteblockquote_length_threshold(int, default: 750) - Min length to use blockquotetry_replace_content_with_title(bool) - Fallback strategy when message too long
Default template (config/jinja_templates/default.j2) options:
show_title(bool, default: true) - Show entry titleshow_content(bool, default: true) - Show entry contentshow_author(bool, default: false) - Show author nametitle_bold(bool, default: true) - Wrap title in<b>tagstitle_underline(bool, default: true) - Wrap title in<u>tagstitle_as_link(bool, default: true) - Make title a clickable linkplain_link(bool, default: false) - Show raw URL instead of feed name link
"jinja_formatter": {
"show_author": true,
"show_title": true,
"show_content": false,
"title_bold": true,
"title_as_link": true
}Filters posts based on regex pattern matching or media count.
Options:
skip_all(bool, default: false) - Filter out all entries (temporarily disable a channel)patterns(list) - Regex patterns to matchmatch_title(bool, default: true) - Check titlematch_content(bool, default: true) - Check contentmatch_mode("any" | "all", default: "any") - How patterns combineinvert(bool, default: false) - If true, filter OUT non-matching postsflags(string) - Regex flags (e.g., "IGNORECASE,MULTILINE")min_media_count(int) - Minimum number of media attachments requiredmax_media_count(int) - Maximum number of media attachments allowed
Extra flags support:
extra_flags.skip(bool) - Skip individual feeds without creating separate channels
Filter out ads/sponsored posts:
"content_filter": {
"patterns": ["Advertisement", "Sponsored"],
"match_mode": "any"
}Keep only posts about Python:
"content_filter": {
"patterns": ["[Pp]ython"],
"invert": true
}Skip a specific feed:
"feeds": {
"https://example.com/noisy-feed": {
"extra_flags": {"skip": true}
}
}Downloads videos using yt-dlp. Requires yt-dlp to be installed (included in Docker image).
Options:
url_patterns(list) - Regex patterns to match video URLs (default: YouTube, Reddit video)search_in(string, default: "link") - Where to search for URLs:"link"- Only search in entry.link"title"- Only search in entry.title"content"- Search in entry.content (all matches)"content_first"- Only first match in entry.content"all"- Search everywhere
cookies_file(string) - Path to cookies.txt file (e.g.,config/cookies.txt)proxy_file(string) - Path to file containing proxy URL (e.g.,config/proxy.txtwithsocks5://host:port)max_filesize(int, default: 50) - Max file size in MBmax_duration(int, default: 900) - Max video duration in secondsdownload_timeout(int, default: 300) - Download timeout in secondsquality(string, default: "best[height<=720]/bv+ba/bv") - yt-dlp format selectionextract_audio(bool, default: false) - Extract audio only (mp3 with thumbnail)use_deno_js_runtime(bool, default: false) - Use Deno JS runtime for YouTubedeno_path(string, default: "/usr/local/bin/deno") - Path to Deno binary
"ytdlp_downloader": {
"url_patterns": ["https?://(?:www\\.)?youtube\\.com/watch"],
"search_in": "link",
"max_filesize": 50,
"quality": "best[height<=720]"
}Appends custom text to formatted messages. Must run after jinja_formatter. Useful for adding #hashtags, @mentions, or other custom text to posts.
Options:
text(string) - The text to appendposition(string, default: "suffix") - Where to place text: "prefix" or "suffix"
Extra flags support:
extra_flags.append_text(string or dict) - Per-feed override. Can be a simple string or dict withtextandposition.
"processing": {
"jinja_formatter": {},
"append_text": {
"text": "#news"
}
}Per-feed override:
"feeds": {
"https://example.com/tech": {
"extra_flags": {"append_text": "#tech"}
},
"https://example.com/sports": {
"extra_flags": {"append_text": {"text": "#sports #news\n\n", "position": "prefix"}}
}
}- RSS-Bridge/rss-bridge — Create feeds from Youtube, Vk, Telegram channels and more.
- trashhalo/reddit-rss — Media-rich reddit RSS. Doesn't require a reddit account.
Once you configured a feed you can use /test command to test it with the whole processing pipeline (/test command will try finding the url somewhere in config).
Reddit with media
You must self-host trashhalo/reddit-rss to get videos and high resolution pirctures in RSS feed.
{
"id": -100123,
"name": "vidya reddit",
"enable_preview": false,
"processing": {
"ytdlp_downloader": {
"search_in": "link",
"max_duration": 200
},
"media_extract": {
"skip_if_has_media": true
},
"html_to_telegram": {},
"jinja_formatter": {
"plain_link": false,
"content_use_blockquote": true,
"blockquote_only_if_exceeds": true
}
},
"feeds": {
"https://reddit-rss.example.com/r/shittydarksouls/top.json?sort=top&t=week": {
"name": "reddit.com/r/shittydarksouls"
}
}
}
YouTube
I suggest self-hosting RSS-Bridge/rss-bridge but you can use the public instance.
Will download short videos, and just link the large ones. show_content": false to hide video description.
{
"id": -100123,
"name": "vidya youtube",
"processing": {
"ytdlp_downloader": {
"search_in": "link",
"max_duration": 200
},
"html_to_telegram": {},
"jinja_formatter": {
"show_content": false
}
},
"feeds": {
"https://rss-bridge.org/?action=display&bridge=YouTubeFeedExpanderBridge&channel=UC2oWuUSd3t3t5O3Vxp4lgAA&format=Atom": {},
"https://rss-bridge.org/?action=display&bridge=YouTubeFeedExpanderBridge&channel=UCDPG5a6rinyhES4wTLrGfig&format=Atom": {}
}
}Music/Audio from YouTube
"show_content": false to hide video description. cookies_file and proxy_file are optional.
{
"id": -100123,
"name": "Ringtone bangers",
"processing": {
"ytdlp_downloader": {
"cookies_file": "config/cookies.txt",
"proxy_file": "config/socks5.txt",
"extract_audio": true,
"search_in": "link",
"max_duration": 2160
},
"media_extract": {
"download_media": true,
"max_media_size": 20971520,
"download_timeout": 30
},
"html_to_telegram": {},
"jinja_formatter": {
"show_content": false
}
},
"feeds": {
"https://rss-bridge.org/bridge01/?action=display&bridge=YouTubeFeedExpanderBridge&channel=UCjEk4ipFLqcdZ_m7uqItTIw&format=Atom": {}
}
}
Telegram
Telegram natively embeds all media nicely, skip everything other than link.
{
"id": 123,
"name": "animals telegram",
"processing": {
"html_to_telegram": {},
"jinja_formatter": {
"show_title": false,
"show_content": false
}
},
"feeds": {
"https://rss-bridge.org/?action=display&bridge=TelegramBridge&username=@birblife&format=Atom": {}
}
}
VKontakte
You must self-host RSS-Bridge/rss-bridge and use Vk api by using your Vk.com account and creating a Vk app. See rss-bridge docs.
{
"id": -100123,
"name": "hema news vk",
"processing": {
"media_extract": {},
"html_to_telegram": {},
"jinja_formatter": {
"show_title": false,
"try_replace_content_with_title": true,
"content_use_blockquote": true,
"blockquote_only_if_exceeds": true
}
},
"feeds": {
"https://rss.example.com/?action=display&bridge=Vk2Bridge&u=wildarmory&format=Atom": {},
"https://rss.example.com/?action=display&bridge=Vk2Bridge&u=ur_for_hema&format=Atom": {}
}
}Using "content_use_blockquote": true will hide long text in exandable block.
Text exceeding telegram limit of 4096 will be excluded entirely.
- Rongronggg9/RSS-to-Telegram-Bot — Better multi-user support



