1 import express, {Request, Response} from 'express';
2 import { join } from 'path';
3 import { query } from './lib/db';
4 import { v4 as uuidv4 } from 'uuid';
5 import { ingest, ingestSingle } from './ingester';
6 import {RSSParser} from './parsers/rss';
7 import bodyParser from 'body-parser';
8 import {BaseParser} from './parsers/base';
11 const app = express();
13 app.use(bodyParser.json());
14 app.use(bodyParser.urlencoded({ extended: true}));
15 app.use(express.static(join(__dirname, '..', 'html')));
16 app.use((req, res, next) => {
17 console.log(`${req.method} ${req.path}`);
21 type WrappedApiHandler = (req: Request, res: Response) => Promise<any>;
23 function apiWrapper(method: 'get' | 'post' | 'delete', endpoint: string, fn: WrappedApiHandler, view?: (args: any) => string) : void {
24 app[method](endpoint, async(req, res) => {
26 const output = await fn(req, res);
27 if(req.headers['content-type'] === 'application/json') {
33 const viewOutput = view(output);
34 if(viewOutput.length) {
52 function apiGet(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
53 apiWrapper('get', endpoint, fn, view);
56 function apiPost(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
57 apiWrapper('post', endpoint, fn, view);
59 function apiDelete(endpoint: string, fn: WrappedApiHandler, view?: (arr: any) => string): void {
60 apiWrapper('delete', endpoint, fn, view);
63 apiPost('/login', async (req, res): Promise<any> => {
66 apiPost('/feeds', async (req, res): Promise<any> => {
67 // get info about the feed
68 const url = req.body.link;
70 let parser: BaseParser;
72 // based on the url, we should figure out if this is a reddit or rss feed
73 parser = new RSSParser();
75 const feedData = await parser.parse(url);
77 const title = feedData.title;
82 query.addFeed.run(id, title, url);
84 await ingestSingle(id);
86 res.setHeader('HX-Trigger', 'newFeed');
95 apiGet('/feeds', async (req, res): Promise<any> => {
96 const feeds = query.getFeedList.all();
98 const unread_count = query.getUnreadCountForAll.all();
100 const feedsWithUnread = feeds.map(feed => {
101 const unread = unread_count.filter(i => i.feed_id === feed.id);
106 unread: (unread && unread.length) ? unread[0].unread : 0
111 feeds: feedsWithUnread
113 }, (output: any): string => {
114 const feeds = output.feeds;
115 return `<ul class="list">${feeds.map((feed: any, idx: number) =>{
116 const display = feed.unread ? `(${feed.unread})` : '';
117 const first = idx === 0;
119 return `<li><a href="#" class="${first ? 'active' : ''} ${feed.unread ? 'unread' : ''}" data-actions="activate" hx-get="/feeds/${feed.id}/items" hx-trigger="${first ? 'load,' : ''}click" hx-target="#list-pane" data-feed-id="${feed.id}">${feed.title}
120 <span class="unread-count">${display}</span>
122 }).join("\n")}</ul>`;
125 function reasonable(date: Date): string {
126 const month = date.getMonth() < 10 ? `0${date.getMonth()}` : date.getMonth();
127 const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
128 return `${date.getFullYear()}-${month}-${day}`;
131 apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
132 const id = req.params.feed_id;
133 return query.getFeedInfo.get(id);
134 }, (feed: any): string => {
136 <b>Feed:</b> ${feed.title}<br>
137 <b>Link:</b> ${feed.link}<br>
141 apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
142 const id = req.params.feed_id;
144 items: query.getFeedsFor.all(id, 0),
145 info: query.getFeedInfo.get(id)
147 }, (feedData: any): string => {
150 <div id="feed-actions">
151 <a href="#" class="btn" hx-post="/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
152 <a href="#" class="btn" hx-delete="/feeds/${feedData.info.id}" hx-trigger="click" hx-confirm="Are you sure you want to delete this feed?">Delete Feed</a>
154 <b>Feed: </b>${feedData.info.title}<br>
157 <table><thead><tr><th style="width: 80%">Title</th><th>Publish Date</th></head></table>
159 <ul class="scrollable list" id="feed-item-list">
160 ${feedData.items.map((item: any, index: number) => {
161 const read = !!item.read_at;
162 const first = index === 0;
164 <a href="#" class="${first ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/feeds/${item.feed_id}/items/${item.id}" hx-trigger="click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
165 <span class="date">${reasonable(new Date(item.pub_date * 1000))}</span>
173 apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise<any> => {
174 const id = req.params.feed_id;
175 query.readAllItems.run(id);
179 apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
180 const feed_id = req.params.feed_id;
181 const item_id = req.params.item_id;
183 query.readItem.run(Date.now(), item_id);
184 return query.getFeedItemInfo.get(item_id, feed_id);
186 }, (output: any): string => {
189 <b>Title:</b> ${output.title}<br>
190 <b>Link:</b> <a href="${output.link}" target="_blank">${output.link}</a><br>
191 <b>Posted:</b> ${new Date(output.pub_date * 1000)}<brj
192 <b>Read: </b> ${new Date(output.read_at || undefined)}
194 <div class="scrollable" id="feed-content">${output.content}</div>
196 return output.content;
199 apiDelete('/feeds/:feed_id', async (req, res) => {
200 const id = req.params.feed_id;
202 query.deleteFeed.run(id);
203 res.setHeader('HX-Trigger', 'newFeed');
207 async function periodicIngest() {
209 setTimeout(periodicIngest, 1000 * 60 * 10);
214 app.listen(process.env.PORT || 8000, () => {
215 console.log('Listening on port', process.env.PORT || 8000);