# ELF_RSS: a Nonebot2 plugin that turns chat messages into RSS subscriptions

> ELF_RSS is a QQ bot plugin for subscribing to RSS feeds and pushing updates as chat messages, built on Nonebot2 with a OneBot adapter. Its distinguishing feature is that a subscription is added by sending the bot a command, rather than by editing a configuration file on the server, and the plugin also handles translation, image compression, torrent handling and PikPak uploads.

**Quan666/ELF_RSS** — QQ机器人 RSS订阅 插件，订阅源建议选择 RSSHub

- Repository: https://github.com/Quan666/ELF_RSS
- Website: https://myelf.club/archives/221
- Stars: 609 · Forks: 67
- Language: Python
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/quan666-elf-rss

## Adding a subscription is a chat message, not a config edit

ELF_RSS is a QQ bot plugin written in Python that subscribes to RSS and pushes updates in real time as QQ messages. The README states the difference from other plugins plainly: most require editing the configuration on the server to add a subscription, whereas this plugin only needs a QQ message sent to the bot to add one dynamically.

That is the whole product decision, and it changes who the plugin is for. A bot whose subscriptions live in a config file is administered by the person who owns the server. A bot whose subscriptions are added by message can be used by the people in the chat, each with their own feeds, which is the case for a group where members want a particular series or blog followed automatically.

Subscription targets are flexible on that basis: single or multiple subscriptions are supported for QQ users, QQ groups and QQ channels, and each subscription carries its own settings.

The plugin also states its own three selling points at the top of the README: easy to use commands, more regular code that is convenient to port into your own bot, and use of the new Nonebot2 framework. The default branch is 2.0, which is the line the current documentation and the 2.0 user guide describe, with the 1.0 guide kept alongside it.

The author is candid about the state of the code, saying it is the first fairly complete and practical project written in Python, that the code is not pretty, and that it is being refactored. That is useful information for an adopter, because it sets the expectation that the internals will move.

The licence is GPL-3.0, recorded as GPL-3.0-only in the package metadata, and the last push was on 2026-10-20.

## The feature list goes well beyond pushing a link

The functional list is longer than a typical RSS-to-chat bridge, and the extras are where the engineering is.

There is a set of commands for adding, deleting, querying and modifying RSS subscriptions, and an interactive flow for adding RSSHub subscriptions. Subscriptions can be translated, using Google machine translation by default with Baidu translation selectable. Per-subscription settings cover update frequency, translation, title only, images only and similar switches.

Then the parts that are specific to a chat platform. Images are compressed before sending, because a chat client has size limits and a full resolution image will simply fail. Messages can be deduplicated by link, by title or by image, so the same item arriving through two feeds does not appear twice. There is a setting for how many images to send, to avoid flooding the group, and a setting for content to strip from the body, with regular expression support, for removing boilerplate or tracking parameters.

Two features stand out because they reach outside the chat entirely. Torrent downloads from an RSS source are downloaded and uploaded to the group file, which the TODO list marks as done and describes as suited to anime series subscriptions. And there is offline download to PikPak cloud storage, which the README says is convenient for following series.

The dependency list explains the mechanics behind each of those. feedparser handles the feed format, nonebot-plugin-apscheduler drives the update schedule, deep-translator does the translation, Pillow and ImageHash handle image compression and the image-based deduplication, magneturi parses magnet links, python-qbittorrent is the client for the torrent download, pikpakapi is the PikPak client, and tinydb stores the subscription state.

ImageHash for deduplication by image is a neat choice. Comparing perceptual hashes catches the case where the same picture arrives from two feeds at different sizes or encodings, which a URL comparison would miss.

## RSSHub, Nonebot2 and Onebot are the load-bearing dependencies

The project acknowledges five projects and services, and they explain the architecture.

RSSHub is the feed source. The package description says the subscription source is recommended to be RSSHub, and the interactive subscription flow is built around it. RSSHub turns a site without a feed into a feed, and this plugin treats those routes as the normal case rather than an exception. The plugin is therefore not limited to sites that publish RSS themselves, which is the practical reason to run it at all.

Nonebot2 is the bot framework, with the fastapi extra enabled, and nonebot-adapter-onebot at 2.4.6 is the protocol adapter. The Onebot adapter is the connection to a QQ implementation, and the README's acknowledgements list CoolQ, coolq-http-api and go-cqhttp, which are the implementations on the other side of that protocol.

The scheduling side is nonebot-plugin-apscheduler, which is what makes per-subscription update frequency possible. Each subscription can be checked on its own schedule rather than on one global cron.

The rest of the dependency set is ordinary application code: aiohttp with the speedups extra for HTTP, arrow for time handling, async-timeout, bbcode for BBCode rendering, cachetools for caching, emoji, tenacity for retries, yarl for URL handling, pydantic for validation and pyquery for HTML scraping.

One detail in the pydantic constraint is worth noting if you hit install problems. It is pinned as >=1.10.26,<3.0.0 with 2.5.0 and 2.5.1 excluded, so two specific releases are excluded from an otherwise wide range. Those exclusions exist for a reason that the manifest does not explain, and they are the kind of thing that bites when you resolve dependencies a year from now.

The Python requirement is the other thing to look at. The manifest declares python = ">=3.15.0,<4.0", while the README recommends Python 3.10 or newer and the Dockerfile is based on a Python 3.10 image. That gap is a real inconsistency in the project rather than something you misread.

## The container runs nb run, and the compose file decides the worker count

The Dockerfile is short and shows the deployment shape.

```dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10

RUN python3 -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple

COPY . /app/

RUN python3 -m pip install -r requirements.txt

CMD nb run
```

The base image is the FastAPI and uvicorn and gunicorn image, and the pip index is pointed at a mirror. The source is copied in, requirements.txt is installed, and the entry command is nb run, which is the Nonebot2 launcher.

The compose file shows how the container is wired.

```yaml
version: "2"
services:
  # 其他配置参考 https://hub.docker.com/r/tiangolo/uvicorn-gunicorn-fastapi/
  nonebot:
    build: .
    volumes:
      - "/etc/localtime:/etc/localtime"
      - "./:/app/"
    ports:
      - "8080:8080" # 映射端口到宿主机 宿主机端口:容器端口
    env_file:
      - ".env.prod" # fastapi 使用的环境变量文件
    environment:
      - ENVIRONMENT=prod # 配置 nonebot 运行环境，此项会被 .env 文件覆盖
      - APP_MODULE=bot:app # 配置 asgi 入口
      - SECRET # 通过 SECRET=xxx nb up -d 传递密钥
      - ACCESS_TOKEN # 通过 ACCESS_TOKEN=xxx nb up -d 传递密钥
      - MAX_WORKERS=1 # 如果你有多个QQ，且存在 self_id 指定，多个 worker 会导致无法找到其他 websocket 连接
    network_mode: bridge
```

Two mount points matter. The host timezone is mounted in, which keeps the log timestamps and the scheduled update times aligned with local time rather than UTC. And the whole project directory is mounted at /app, so the source is live inside the container and a change on the host takes effect without rebuilding.

The environment block is where the operational subtlety sits. APP_MODULE points at bot:app, which is the ASGI entry point in the bot.py file at the repository root. SECRET and ACCESS_TOKEN are passed through with nb up -d.

And MAX_WORKERS=1 carries a comment that is the most useful thing in the file: if you have several QQ accounts and use the self_id setting, multiple workers will fail to find the other websocket connections. With more than one bot account, each account needs to be reachable by the process, and one worker cannot see the others' connections. That is the kind of constraint that turns into an afternoon of debugging if you do not know it in advance.

The port mapping is 8080 to 8080, which is the HTTP port the FastAPI side listens on.

## Documentation lives in docs/, and the TODO list is mostly ticked

The documentation is organised as a set of Markdown files in the docs/ directory rather than as a wiki or a separate site, and the README links five of them: a deployment guide, the 2.0 user guide, the older 1.0 user guide, a FAQ and a changelog.

Keeping the 1.0 guide alongside the 2.0 one is a small but useful decision for an adopter, because the plugin was substantially rewritten for the Nonebot2 rewrite and a bot operator moving from the old line needs to know that the commands and configuration differ rather than assuming they are the same.

The TODO list in the README is a good indicator of where the project has been. Six items are marked complete: protecting subscription information so subscribed QQ accounts and groups are not printed in group chats, a stronger update time setting, automatic torrent download and upload to the subscribed group for series subscriptions, pausing update checks, regular expression matching of subscription names, and performance work to replace synchronous operations with asynchronous ones wherever possible.

Every one of those is checked, and the list is short. The privacy item is the one worth flagging for a group deployment. A bot that pushes your subscriptions to a group chat can leak who is subscribed to what, so the fact that subscribed accounts and groups are kept out of group output is a deliberate design decision rather than an accident.

The regular expression support for subscription names and the pause control are also practical features that most simpler bridges do not have. Pausing is what you want when a subscription turns noisy.

The homepage of the project is a blog post at myelf.club/archives/221, and the documentation site for the deployment details is not on a separate domain but inside the repository, which means it is versioned alongside the code.

## Where ELF_RSS is the wrong tool

The first wrong-tool case is a deployment that needs a stable, documented interface. The author says plainly that the code is being refactored, the default branch is 2.0 rather than a stable tag, and the most recent release tags are v2.6.25 from 2024-09-16, v2.6.24 from 2024-09-14 and v2.6.23 from 2024-07-17. So there are two years of unlabelled work on the branch, and a plugin you want to pin to a version has few recent versions to choose from.

The second case is a Python version mismatch, and it is not hypothetical. The package metadata requires Python 3.15 or newer, the Dockerfile ships a 3.10 base image, and the README recommends 3.10 or newer. Whichever is right, three sources disagree, and you will find out which by trying to install it.

The third case is a deployment on a QQ implementation that is not reachable through the OneBot protocol the plugin expects. The plugin depends on nonebot-adapter-onebot, so the QQ side has to speak that protocol, and the README's acknowledgements point at CoolQ, coolq-http-api and go-cqhttp as the implementations it was built against. A bot platform without a OneBot implementation is a different integration entirely.

The fourth case is a team that wants subscriptions in a config file under version control. The dynamic subscription model is the point of the plugin, and it is the wrong model for an environment where the set of feeds is an auditable part of the deployment.

There is a compliance point about the download features that is worth stating rather than glossing. The plugin can download torrents from an RSS source and upload them to a group file, and it can download to PikPak. Those are conveniences for people following series, and they are also the features most likely to conflict with the rules of the sites you point it at and with the rules of the platform hosting the uploads. Anyone deploying this should check that before enabling them, rather than after.

On the licence, GPL-3.0-only is a copyleft licence with real obligations for anyone who distributes a modified version of this plugin. For a personal bot this is a non-issue. For a company that wants to run the plugin internally, it does not bind you, since GPL does not reach internal use, but redistributing a modified version does.

## Conclusion

ELF_RSS suits someone running a QQ bot on the Nonebot2 stack who wants their group members to manage their own RSS subscriptions without touching the server configuration, since adding a feed is a chat message. It does not suit a deployment that needs a documented, versioned subscription list or a stable public interface, because the plugin is explicitly being refactored and the default branch has moved to a 2.x line. Before adopting, check the Python version your host actually runs against the pyproject requirement of 3.15 or newer, since the README recommends 3.10+ while the manifest pins a much higher floor, and read the deployment guide in the docs/ directory before wiring up a OneBot implementation.

## FAQ

### What is ELF_RSS?

ELF_RSS is a QQ bot plugin written in Python that subscribes to RSS feeds and pushes updates in real time as QQ messages. It is built on the Nonebot2 framework with a Onebot adapter, and the package metadata records the licence as GPL-3.0-only.

### How do I add an RSS subscription to ELF_RSS?

You send the bot a command. Unlike plugins that require editing the server configuration to add a feed, ELF_RSS adds subscriptions dynamically through QQ messages, and it also has an interactive flow for adding RSSHub subscriptions. Commands exist to add, delete, query and modify subscriptions.

### What can ELF_RSS do besides pushing feed items?

Subscriptions can be translated with Google machine translation or Baidu translation, images are compressed before sending, messages can be deduplicated by link, title or image, and per-subscription settings cover update frequency, title only and images only. There is also torrent download with upload to the group file, and offline download to PikPak cloud storage.

### How do I run ELF_RSS with Docker?

The Dockerfile builds from a uvicorn and gunicorn FastAPI image, installs requirements.txt and starts the bot with nb run. The compose file mounts the project directory and the host timezone, maps port 8080, and sets ENVIRONMENT, APP_MODULE, SECRET, ACCESS_TOKEN and MAX_WORKERS.

### Why does the ELF_RSS compose file set MAX_WORKERS to 1?

The comment in the compose file explains that with several QQ accounts and a self_id setting, multiple workers cause the bot to fail to find the other websocket connections. Each account has to be reachable by the process, so the worker count is constrained by the number of connected accounts.

## Sources

- [License: GPL-3.0](https://github.com/Quan666/ELF_RSS/blob/2.0/LICENSE)
- [Project website](https://myelf.club/archives/221)
- [Quan666/ELF_RSS on GitHub](https://github.com/Quan666/ELF_RSS)
- [README](https://github.com/Quan666/ELF_RSS/blob/2.0/README.md)
- [Releases](https://github.com/Quan666/ELF_RSS/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/quan666-elf-rss
