ijsbakje

readstack

A single-user RSS aggregator.

License: MIT License

App logo

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:/data

You 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 version

For 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=1234

The 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.