Awesome
Grapevine RSS Aggregator
RSS Aggregator written in Typescript
Motivation
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 **
Setup
Container: https://hub.docker.com/r/mrbond/grapevine-rss-aggregator/
Web Front End: Grapevine RSS Reader
Database
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
Creating a user
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
Running in Production
datbase information can be passed into the application via ENV variables.
- DB_HOST
- DP_PORT
- DB_USER
- DB_PASSWD
Docker Compose
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
API
Feeds
Create
URL: /api/v1/feed
Method: POST
Payload:
{
title: Joi.string().required(),
url: Joi.string().uri().required(),
}
Update
URL: /api/v1/feed
Method: PUT
Payload:
{
id: Joi.number().integer().min(1).required(),
title: Joi.string().required(),
url: Joi.string().uri().required(),
}
Get
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(),
}
Delete
URL: /api/v1/feed/{id}
Method: DELETE
Response:
{
"message": "successfully deleted feed"
}
Groups
Add Group
URL: /api/v1/group
Method: POST
Payload:
{
name: Joi.string().required(),
}
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
Update Group
URL: /api/v1/group/{id}
Method: PUT
Payload:
{
name: Joi.string().required(),
}
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
Get List of Groups
URL: /api/v1/group
Method: GET
Response:
[
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
]
Get Group
URL: /api/v1/group/{id}
Method: GET
Response:
{
id: Joi.number().integer().min(1).required(),
name: Joi.string().required(),
}
Delete Group
URL: /api/v1/group/{id}
Method: DELETE
Feed Groups
Add Feed to Group
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),
}
Delete Feed from Group
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),
}
Get Groups for Feed
URL: /api/v1/feed/{id}/groups
Method: GET
Response:
{
groups: Joi.array().items(joiGroupResponse),
}
Get Feeds in group
URL: /api/v1/group/{id}/feeds
Method: GET
Response:
{
feeds: Joi.array().items(joiRssFeedApiResponse),
}
Items
Get Items in Feed
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(),
}
]
Get items
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(),
}
]
Update item status
URL: /api/v1/item/{id}/status
Method: POST
Payload:
{
flag: Joi.string().only(
ItemFlags.read,
ItemFlags.unread,
ItemFlags.starred,
ItemFlags.unstarred,
),
}
Update multiple items statuses
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()),
}
TODO
- 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