Skip to main content

Forward links from RSS/Atom feeds to messengers

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

feedforbot-5.1.0.tar.gz (109.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

feedforbot-5.1.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file feedforbot-5.1.0.tar.gz.

File metadata

  • Download URL: feedforbot-5.1.0.tar.gz
  • Upload date:
  • Size: 109.9 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for feedforbot-5.1.0.tar.gz
Algorithm Hash digest
SHA256 b02b5e2e9ca1098ca39c40619ff81bf9802660da913411a7feca42bf82fc91fd
MD5 4b2af47325163199d8a25aac2e6f0938
BLAKE2b-256 21bb740bf4a502481921931221002b257cdb4e2955cc06321754029da299c62c

See more details on using hashes here.

File details

Details for the file feedforbot-5.1.0-py3-none-any.whl.

File metadata

  • Download URL: feedforbot-5.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for feedforbot-5.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 462ebca2f33c11f2f74cd46b393eb5b4697bf40d401ae9b5f137ebabb2867b81
MD5 81de6a1077f128dda81bd4da714c4fff
BLAKE2b-256 dc5d9b0e4ba665a05bae2a8e2b42c51c94f9da4caf9e542f80d1b53887a72cec

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page