readstack
A single-user RSS aggregator.
License: MIT License
Readstack
Readstack is a single-user RSS aggregator with a GReader compatibility layer.
Quickstart
Readstack is available as a Docker image which you can start as
docker run --rm nchagnet/readstack:latest \
--name readstack \
-v /opt/readstack/data:/dataYou can also build the application from source using cargo install --locked readstack and then run readstack serve.
Authentication is handled via tokens generated through readstack create-token (be careful, the token will be displayed only once!) If you are running the Docker container, generate a token via
docker exec -it readstack "readstack create-token"Tokens have no expiry dates.
The app exposes its API on localhost:8080 (which you can then place behind a reverse proxy) and can be accessed via HTTP requests.
API
The CLI should be used for admin operations like token management
readstack: a single-user RSS aggregator
Usage: readstack <COMMAND>
Commands:
serve Serve the http server
import Import feeds from an OPML document
export Write the feeds as an OPML document
clear Delete all the content, and optionally the tokens
create-token Generate an authentication token and print it one time
delete-token Delete an authentication token
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print versionFor feed management, use the HTTP API.
Authentication
All routes except /health require a token:
Authorization: Bearer <token>Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /health |
Status of the server and the database. It needs no token. Returns 200 or 503. |
| GET | /feeds |
List the feeds. |
| POST | /feeds |
Add a feed and fetch it now. Returns 201. |
| GET | /feeds/{id} |
Return one feed. |
| PATCH | /feeds/{id} |
Update the title or the poll frequency. |
| DELETE | /feeds/{id} |
Delete a feed and its items. |
| PUT | /feeds/{id}/folder |
Set the folder of a feed. The folder is created when it is new. |
| DELETE | /feeds/{id}/folder |
Remove the folder of a feed. |
| POST | /feeds/{id}/refresh |
Fetch the feed now. |
| POST | /feeds/{id}/mark-read |
Mark every item of the feed read. |
| GET | /feeds/{id}/items |
List one page of the items of the feed. |
| GET | /items |
List one page of the items of every feed. |
| POST | /items |
Save a web page for later reading. Returns 201. |
| GET | /items/{id} |
Return one item, with its content and author. |
| DELETE | /items/{id} |
Delete one item. |
| PATCH | /items/{id}/read |
Set the read status. |
| PATCH | /items/{id}/starred |
Set the starred status, which controls the read later list. |
| POST | /mark-all-read |
Mark every item of every visible feed read. |
| GET | /unread-counts |
Count the unread items of every stream. |
| GET | /folders |
List the folders and the number of feeds in each one. |
| PATCH | /folders/{name} |
Rename a folder. |
| DELETE | /folders/{name} |
Delete a folder. Its feeds become folderless. |
| GET | /export |
Download the feeds as an OPML document. |
| POST | /import |
Import the feeds of an OPML document. |
Requests
Add a feed:
{
"url": "https://example.com/feed.xml",
"title": "Example",
"poll_frequency_minutes": 30,
"folder": "Tech"
}Only url is required. Without title, the feed provides it. poll_frequency_minutes defaults to 30 and must be at least 15. The folder is created when it is new.
Update a feed. Send at least one field:
{ "title": "New title", "poll_frequency_minutes": 60 }Change a folder:
{ "name": "Tech" }Use it as the body of PUT /feeds/{id}/folder and PATCH /folders/{old-name}.
Change the state of an item:
{ "read": false }{ "starred": true }Save a page:
{ "url": "https://example.com/article" }The server fetches the page and stores it as a starred item. Save a page that exists already to refresh it.
Import an OPML document. Send exactly one of url or content:
{ "url": "https://example.com/feeds.opml" }{ "content": "<?xml version=\"1.0\"?><opml ...>" }The document must not be larger than 5 MB. The request body must not be larger than 6 MB.
List items. unread and before work on /items and /feeds/{id}/items. starred works on /items only:
GET /items?unread=true&starred=true&limit=50&before=1234The read later list is GET /items?starred=true. It holds the starred items and the saved pages.
Responses
Feed:
{
"id": 1,
"url": "https://example.com/feed.xml",
"title": "Example",
"domain": "example.com",
"folder": "Tech",
"last_fetched": 1760000000,
"poll_frequency_minutes": 30,
"site_url": "https://example.com",
"description": "An example feed",
"last_error": null,
"last_error_at": null
}last_error and last_error_at describe the last failed fetch. They are null after a successful fetch. site_url, description, folder, and last_fetched can be null.
Item:
{
"id": 42,
"feed_id": 1,
"guid": "https://example.com/post/1",
"title": "A post",
"link": "https://example.com/post/1",
"published": 1760000000,
"read": false,
"starred": false,
"content": "<p>The content.</p>",
"author": "Jane Doe"
}published and author can be null. content is the item content, usually HTML.
Unread counts:
{
"total": { "count": 3, "newest": 1760000000 },
"starred": { "count": 1, "newest": 1759000000 },
"feeds": [{ "feed_id": 1, "count": 3, "newest": 1760000000 }],
"folders": [{ "name": "Tech", "count": 3, "newest": 1760000000 }]
}newest is the publication time of the newest unread item, or null. starred includes the saved pages.
Folder:
{ "name": "Tech", "feed_count": 2 }Import result:
{ "added": 5, "skipped": 1 }Health:
{ "status": "ok" }status is ok or error.
Google Reader API
The server implements the Google Reader API for existing clients. The routes are served at the root and under /api/greader.php, which is the address that FreshRSS clients expect. This layer accepts the token in the Authorization header:
Authorization: Bearer <token>
Authorization: GoogleLogin auth=<token>A write request can send the token in the T parameter instead:
T=<token>GET or POST /accounts/ClientLogin takes Email=<name>&Passwd=<token> as a query, a urlencoded form, or a multipart form, and returns three text lines:
SID=<token>
LSID=<token>
Auth=<token>| Method | Path |
|---|---|
| GET | /reader/api/0/token |
| GET | /reader/api/0/tag/list |
| GET | /reader/api/0/unread-count |
| GET | /reader/api/0/user-info |
| GET | /reader/api/0/subscription/list |
| GET | /reader/api/0/subscription/export |
| GET, POST | /reader/api/0/subscription/quickadd |
| POST | /reader/api/0/subscription/import |
| POST | /reader/api/0/subscription/edit |
| GET, POST | /reader/api/0/stream/items/ids |
| GET, POST | /reader/api/0/stream/items/contents |
| GET, POST | /reader/api/0/stream/contents |
| GET | /reader/api/0/stream/contents/{*stream} |
| POST | /reader/api/0/tag/edit |
| POST | /reader/api/0/rename-tag |
| POST | /reader/api/0/disable-tag |
| POST | /reader/api/0/edit-tag |
| POST | /reader/api/0/mark-all-read |
| POST | /reader/api/0/mark-all-as-read |
This layer uses query parameters and form bodies, not JSON. Folders use the tag prefix user/-/label/. The read and starred states use the tags user/-/state/com.google/read and user/-/state/com.google/starred. The streams also accept the short names reading-list and starred, and the read and unread streams user/-/state/com.google/read and user/-/state/com.google/unread. For the parameters of each route, see the Google Reader API documentation.