Skip to main content

FeedForBot 📡

PyPI Version PyPI Downloads Docker Pulls GHCR Lint Ruff

RSS/Atom in, Telegram out. 🪄 Fresh entries arrive, templated into tidy messages, deduped via your cache of choice — on a cron schedule, with a healthcheck and graceful shutdown baked in.

✨ Features

  • 📬 Multiple feeds — any number of listeners/transports, each on its own cron
  • 🎨 Jinja2 templates — sandbox + HTML autoescape, full access to article fields
  • 💾 Pluggable cache — files, in-memory, or Redis; first run silent to avoid flood
  • 🩺 Healthcheck — HTTP /health for Docker/Kubernetes probes
  • 🛡️ Resilient delivery — auto-retry on Telegram flood-wait, skip malformed entries
  • 🧹 Graceful shutdown — SIGTERM/SIGINT drain ticks and close handles cleanly
  • 🐛 Sentry integration — optional error tracking
  • 🐳 Docker ready — multi-arch GHCR + Docker Hub images, runs as non-root
  • 🧩 Protocol-driven — custom listeners/transports/caches via simple protocols

📦 Installation

Requires Python 3.10+.

pip install feedforbot -U

For the full CLI (Click, structlog, YAML config, Sentry, Redis):

pip install "feedforbot[cli]" -U

🚀 Quick start

🐍 As a library

from feedforbot import Scheduler, TelegramBotTransport, RSSListener

scheduler = Scheduler(
    '*/5 * * * *',
    listener=RSSListener('https://www.debian.org/News/news'),
    transport=TelegramBotTransport(
        token='123456789:AAAAAAAAAA-BBBB-CCCCCCCCCCCC-DDDDDD',
        to='@channel',
    ),
)
scheduler.run()  # blocks, checks the feed every 5 minutes

⚡ Async — multiple schedulers

import asyncio

from feedforbot import Scheduler, TelegramBotTransport, RSSListener

schedulers = [
    Scheduler(
        '*/5 * * * *',
        listener=RSSListener('https://www.debian.org/News/news'),
        transport=TelegramBotTransport(
            token='123456789:AAAAAAAAAA-BBBB-CCCCCCCCCCCC-DDDDDD',
            to='@channel',
        ),
    ),
    Scheduler(
        '0 * * * *',
        listener=RSSListener('https://habr.com/ru/rss/all/all/'),
        transport=TelegramBotTransport(
            token='123456789:AAAAAAAAAA-BBBB-CCCCCCCCCCCC-DDDDDD',
            to='@another_channel',
        ),
    ),
]

async def main() -> None:
    await asyncio.gather(*(s.arun() for s in schedulers))

asyncio.run(main())

📝 CLI with YAML config

Create a schedulers.yml — a top-level list of listener+transport entries:

---
- rule: '*/5 * * * *'
  listener:
    type: 'rss'
    params:
      url: 'https://habr.com/ru/rss/all/all/?fl=ru'
  transport:
    type: 'telegram_bot'
    params:
      token: '123456789:AAAAAAAAAA-BBBB-CCCCCCCCCCCC-DDDDDD'
      to: '@tmfeed'
      template: |-
        <b>{{ TITLE }}</b> #habr
        {{ ID }}
        <b>Tags</b>: {% for category in CATEGORIES %}{{ category }}{{ ", " if not loop.last else "" }}{% endfor %}
        <b>Author</b>: <a href="https://habr.com/users/{{ AUTHORS[0] }}">{{ AUTHORS[0] }}</a>
- listener:
    type: 'rss'
    params:
      url: 'http://www.opennet.ru/opennews/opennews_all.rss'
  transport:
    type: 'telegram_bot'
    params:
      token: '123456789:AAAAAAAAAA-BBBB-CCCCCCCCCCCC-DDDDDD'
      to: '@tmfeed'
      disable_web_page_preview: yes
      template: |-
        <b>{{ TITLE }}</b> #opennet
        {{ URL }}

        {{ TEXT }}

Run:

feedforbot --verbose schedulers.yml

On the first run the cache is populated without sending messages, so existing feed entries won't flood the channel. 🤫

📋 Config reference

Each list entry supports:

Key Description
rule Cron expression (e.g. */5 * * * *), optional — defaults to * * * * * (every minute)
cache_limit Max cache entries kept (keeps N most recent by grabbed_at), optional — unbounded when unset
listener.type rss
listener.params.url Feed URL
transport.type telegram_bot
transport.params.token Telegram Bot API token
transport.params.to Chat ID or @channel name
transport.params.template Jinja2 template string (HTML parse mode), optional — defaults to {{ TITLE }}\n\n{{ TEXT }}\n\n{{ URL }}
transport.params.disable_web_page_preview true / false, optional — defaults to false
transport.params.disable_notification true / false, optional — defaults to false

An empty top-level list is rejected at parse time.

💾 Cache CLI flag

Cache backend selection is a CLI concern (not part of the schedulers file). --cache-dsn takes a URL-style DSN; scheme selects the backend:

DSN Backend
file: (default) Persistent on-disk cache at ~/.feedforbot/
file:///custom/path Persistent on-disk cache at a custom directory
memory: In-process cache (lost on restart)
redis://host:6379/0 Shared Redis cache (requires feedforbot[cli] or pip install redis). Useful for multi-instance deployments and docker-compose setups where a separate Redis container holds state.

🧩 Template variables

All fields from ArticleModel are available in uppercase:

Variable Description
{{ TITLE }} Entry title
{{ URL }} Entry link
{{ ID }} Unique entry identifier
{{ TEXT }} Entry summary / description
{{ CATEGORIES }} List of tags/categories
{{ AUTHORS }} List of authors
{{ GRABBED_AT }} Timestamp when the entry was fetched (UTC)

Templates render in a Jinja2 sandbox with HTML autoescape enabled (messages use parse_mode=HTML). Article fields containing &, <, > are escaped automatically — you don't need to guard against Telegram rejecting the message for invalid HTML.

🎛️ CLI options

Option Description
SCHEDULERS_FILE Positional — path to the YAML schedulers config
-v, --verbose Repeat for more detail (-v INFO, -vv DEBUG, -vvv NOTSET)
-V, --version Print version and exit
--cache-dsn Cache backend DSN (see table above); default file:
--sentry Sentry DSN for error reporting
--healthcheck-port Port for the HTTP healthcheck server (disabled when unset)
--healthcheck-host Bind host for the healthcheck server; default 127.0.0.1. Use 0.0.0.0 inside containers when the port is exposed to the host

🐳 Docker

Images are published to both GHCR and Docker Hub on every release. Tags follow semver: latest, 5, 5.0, 5.0.0.

docker run -v $(pwd)/schedulers.yml:/schedulers.yml \
  ghcr.io/shpaker/feedforbot --verbose /schedulers.yml
docker run -v $(pwd)/schedulers.yml:/schedulers.yml \
  shpaker/feedforbot --verbose /schedulers.yml

The container runs as a non-root user (appuser).

🩺 Healthcheck

The CLI includes a built-in HTTP healthcheck server. Pass --healthcheck-port to expose a lightweight endpoint that responds with 200 OK at /health (other paths return 404 Not Found):

feedforbot --healthcheck-port 8080 --verbose schedulers.yml

The server binds to 127.0.0.1 by default. Pass --healthcheck-host 0.0.0.0 when the port must be reachable from outside the container (e.g. for an external probe); the in-container Docker HEALTHCHECK below works either way since it queries localhost from the same network namespace.

Useful for container orchestrators (Docker HEALTHCHECK, Kubernetes liveness probes, etc.):

# docker-compose.yml
services:
  feedforbot:
    image: ghcr.io/shpaker/feedforbot
    command: ["--healthcheck-port", "8080", "--verbose", "/schedulers.yml"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

🧱 Docker Compose with Redis

For multi-instance deployments or when you want cache state to survive container rebuilds without a host volume for JSON files, run feedforbot alongside a Redis container and point --cache-dsn at it:

# docker-compose.yml
services:
  redis:
    image: redis:7-alpine
    restart: always
    volumes:
      - ./redis_data:/data
    command: ["redis-server", "--save", "60", "1", "--appendonly", "no"]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s

  feedforbot:
    image: ghcr.io/shpaker/feedforbot
    restart: always
    depends_on:
      redis:
        condition: service_healthy
    volumes:
      - ./schedulers.yml:/schedulers.yml:ro
    command: >
      -v
      --cache-dsn redis://redis:6379/0
      --healthcheck-port 8080
      /schedulers.yml
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

A working Ansible playbook that deploys this layout is in tmfeed/deploy.playbook.yml.

📄 License

MIT

Metadata

Release files for feedforbot 5.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for feedforbot 5.1.0
File Size Uploaded
feedforbot-5.1.0.tar.gz 109.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for feedforbot 5.1.0
File Interpreter ABI Platform
feedforbot-5.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 133.0 kB

Release files / feedforbot-5.1.0.tar.gz

Download URL feedforbot-5.1.0.tar.gz
Size 109.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b02b5e2e9ca1098ca39c40619ff81bf9802660da913411a7feca42bf82fc91fd
BLAKE2b-256 checksum
How to use checksums
21bb740bf4a502481921931221002b257cdb4e2955cc06321754029da299c62c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / feedforbot-5.1.0-py3-none-any.whl

Download URL feedforbot-5.1.0-py3-none-any.whl
Size 23.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
462ebca2f33c11f2f74cd46b393eb5b4697bf40d401ae9b5f137ebabb2867b81
BLAKE2b-256 checksum
How to use checksums
dc5d9b0e4ba665a05bae2a8e2b42c51c94f9da4caf9e542f80d1b53887a72cec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page