RSS Aggregator written in Typescript
Being an avid user of RSS since the late 1990s, I have become tired of the RSS software I use disappearing when the software authors lose interest (I'm looking at you Google, Fever, etc ...). Since RSS is s daily use for me, I decided to write, and maintain, my own solution.
Grapevine RSS Aggregator is the backend service. This is the "sync" engine. Run and maintain your own aggregator with an API and connect to it with a client.
Grapevine RSS Reader is the initial frontend service. This is the client, or user interface.
My hope is that other RSS readers will integrate with the Grapevine API.
- Javascript / Typescript API Client : Used in production with the Grapevine RSS Reader
- Dart API Client : Used in the Grapevine RSS Desktop and Mobile Apps **note: in development **
Container: https://hub.docker.com/r/mrbond/grapevine-rss-aggregator/
Web Front End: Grapevine RSS Reader
If you are using docker compose, you can skip to step #2 and just run yarn dbm up
to apply all the database migrations.
-
Create the datbase and user
The following creates a new database called
grapevine_rss
and gives a new usergrapevine
with passwordrss
full access to it.mysql -h 127.0.0.1 -u root -p < ./installation/create_database.sql
-
Run database migrations
yarn dbm up
-
Run datbase migration in docker. Replace
src_grapevine-aggregator_1
with the container iddocker exec -it src_grapevine-aggregator_1 yarn dbm up
Currently all users are read/write. When creating a user a password will be generated. The password will only be displayed once. Be sure to keep the output of the script in a safe place.
To create an account with USERNAME
- While running locally
yarn run add-account -u USERNAME
- While running in Docker
docker exec -e DB_HOST=mysql -e DB_USER=grapevine -e CONFIG_ENV=prod -it CONTAINER_ID yarn run add-account -u USERNAME
datbase information can be passed into the application via ENV variables.
- DB_HOST
- DP_PORT
- DB_USER
- DB_PASSWD
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 12345
MYSQL_DATABASE: node_rss_aggregator
MYSQL_USER: rss
MYSQL_PASSWORD: rss
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
grapevine-rss:
image: mrbond/grapevine-rss-aggregator
environment:
CONFIG_ENV: prod
DB_HOST: mysql
DB_USER: DATABASE_USER
DB_PASSWD: DATABASE_PASSWORD
ports:
- 3000:3000
command: run start
URL: /api/v1/feed
Method: POST
Payload:
{
title: Joi.string().required(),
url: Joi.string().uri().required(),
}
URL: /api/v1/feed
Method: PUT
Payload:
{
id: Joi.number().integer().min(1).required(),
title: Joi.string().required(),
url: Joi.string().uri().required(),
}
URL: /api/v1/feed
Method: GET
Response:
{
id: Joi.number().integer().min(1).required(),
title: Joi.string().required(),
url: Joi.string().uri().required(),
added_on: Joi.number().required(),
last_updated: Joi.number().required(),
}
URL: /api/v1/feed/{id}
Method: DELETE
Response:
{
"message": "successfully deleted feed"
}
URL: /api/v1/group
Method: POST
Payload:
{
name: Joi.string().required(),
}
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
URL: /api/v1/group/{id}
Method: PUT
Payload:
{
name: Joi.string().required(),
}
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
URL: /api/v1/group
Method: GET
Response:
[
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
]
URL: /api/v1/group/{id}
Method: GET
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
URL: /api/v1/group/{id}
Method: DELETE
URL: /api/v1/feed-group
Method: POST
Payload:
{
feed_id: Joi.number().integer().min(1),
group_id: Joi.number().integer().min(1),
}
Response:
{
groups: Joi.array().items(joiGroupResponse),
}
URL: /api/v1/feed-group
Method: DELETE
Payload:
{
feed_id: Joi.number().integer().min(1),
group_id: Joi.number().integer().min(1),
}
Response:
{
groups: Joi.array().items(joiGroupResponse),
}
URL: /api/v1/feed/{id}/groups
Method: GET
Response:
{
groups: Joi.array().items(joiGroupResponse),
}
URL: /api/v1/group/{id}/feeds
Method: GET
Response:
{
feeds: Joi.array().items(joiRssFeedApiResponse),
}
URL: /api/v1/items/feed/{id}/{flags*}
flags: optional. /
delimited list of read
, starred
, unread
, unstarred
Method: GET
Response:
[
{
author: Joi.string().optional().allow(null, ""),
categories: Joi.array().items(Joi.string().allow(null, "")).optional(),
comments: Joi.string().optional().allow(null, ""),
description: Joi.string().optional().allow(null, ""),
enclosures: Joi.array().items(Joi.string().allow(null, "")).optional(),
feed_id: Joi.number().min(1).required(),
guid: Joi.string().required(),
id: Joi.number().min(1).required(),
image: Joi.object().optional(),
link: Joi.string().optional().allow(null, ""),
published: Joi.date(),
read: Joi.boolean().required(),
starred: Joi.boolean().required(),
summary: Joi.string().optional().allow(null, ""),
title: Joi.string().optional().allow(null, ""),
updated: Joi.date(),
}
]
URL: /api/v1/items/{flags*}
flags: optional. /
delimited list of read
, starred
, unread
, unstarred
Method: GET
Response:
[
{
author: Joi.string().optional().allow(null, ""),
categories: Joi.array().items(Joi.string().allow(null, "")).optional(),
comments: Joi.string().optional().allow(null, ""),
description: Joi.string().optional().allow(null, ""),
enclosures: Joi.array().items(Joi.string().allow(null, "")).optional(),
feed_id: Joi.number().min(1).required(),
guid: Joi.string().required(),
id: Joi.number().min(1).required(),
image: Joi.object().optional(),
link: Joi.string().optional().allow(null, ""),
published: Joi.date(),
read: Joi.boolean().required(),
starred: Joi.boolean().required(),
summary: Joi.string().optional().allow(null, ""),
title: Joi.string().optional().allow(null, ""),
updated: Joi.date(),
}
]
URL: /api/v1/item/{id}/status
Method: POST
Payload:
{
flag: Joi.string().only(
ItemFlags.read,
ItemFlags.unread,
ItemFlags.starred,
ItemFlags.unstarred,
),
}
URL: /api/v1/items/status
Method: PATCH
Payload:
{
flag: Joi.string().only(
ItemFlags.read,
ItemFlags.unread,
ItemFlags.starred,
ItemFlags.unstarred,
),
ids: Joi.array().items(Joi.number()),
}
- Parse title from feed when adding a new feed, if none is provided
- Download and store favicon
- Swagger Docs
- Read only users
- Support for Password protected RSS feeds
- Tagging support
- Multiple Users