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('/feeds', async (req, res): Promise<any> => {
64 // get info about the feed
65 const url = req.body.link;
67 let parser: BaseParser;
69 // based on the url, we should figure out if this is a reddit or rss feed
70 parser = new RSSParser();
72 const feedData = await parser.parse(url);
74 const title = feedData.title;
79 query.addFeed.run(id, title, url);
81 await ingestSingle(id);
83 res.setHeader('HX-Trigger', 'newFeed');
92 apiGet('/feeds', async (req, res): Promise<any> => {
93 const feeds = query.getFeedList.all();
95 const unread_count = query.getUnreadCountForAll.all();
97 const feedsWithUnread = feeds.map(feed => {
98 const unread = unread_count.filter(i => i.feed_id === feed.id);
103 unread: (unread && unread.length) ? unread[0].unread : 0
108 feeds: feedsWithUnread
110 }, (output: any): string => {
111 const feeds = output.feeds;
112 return `<ul class="list">${feeds.map((feed: any, idx: number) =>{
113 const display = feed.unread ? `(${feed.unread})` : '';
114 const first = idx === 0;
116 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}
117 <span class="unread-count">${display}</span>
119 }).join("\n")}</ul>`;
122 function reasonable(date: Date): string {
123 const month = date.getMonth() < 10 ? `0${date.getMonth()}` : date.getMonth();
124 const day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
125 return `${date.getFullYear()}-${month}-${day}`;
128 apiGet('/feeds/:feed_id', async (req, res): Promise<any> => {
129 const id = req.params.feed_id;
130 return query.getFeedInfo.get(id);
131 }, (feed: any): string => {
133 <b>Feed:</b> ${feed.title}<br>
134 <b>Link:</b> ${feed.link}<br>
138 apiGet('/feeds/:feed_id/items', async (req, res): Promise<any> => {
139 const id = req.params.feed_id;
141 items: query.getFeedsFor.all(id, 0),
142 info: query.getFeedInfo.get(id)
144 }, (feedData: any): string => {
147 <div id="feed-actions">
148 <a href="#" class="btn" hx-post="/feeds/${feedData.info.id}/items/markAsRead" hx-trigger="click">Mark all as Read</a>
149 <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>
151 <b>Feed: </b>${feedData.info.title}<br>
154 <table><thead><tr><th style="width: 80%">Title</th><th>Publish Date</th></head></table>
156 <ul class="scrollable list" id="feed-item-list">
157 ${feedData.items.map((item: any, index: number) => {
158 const read = !!item.read_at;
160 <a href="#" class="${index === 0 ? 'active': ''} ${read ? '': 'unread'}" data-actions="activate" hx-get="/feeds/${item.feed_id}/items/${item.id}" hx-trigger="${index === 0 ? 'load,': ''}click" hx-target="#reading-pane" data-feed-item-id="${item.id}" data-feed-id="${item.feed_id}">${item.title}
161 <span class="date">${reasonable(new Date(item.pub_date * 1000))}</span>
169 apiPost('/feeds/:feed_id/items/markAsRead', async (req, res): Promise<any> => {
170 const id = req.params.feed_id;
171 query.readAllItems.run(id);
175 apiGet('/feeds/:feed_id/items/:item_id', async (req, res) => {
176 const feed_id = req.params.feed_id;
177 const item_id = req.params.item_id;
179 query.readItem.run(Date.now(), item_id);
180 return query.getFeedItemInfo.get(item_id, feed_id);
182 }, (output: any): string => {
185 <b>Title:</b> ${output.title}<br>
186 <b>Link:</b> <a href="${output.link}" target="_blank">${output.link}</a><br>
187 <b>Posted:</b> ${new Date(output.pub_date * 1000)}<brj
188 <b>Read: </b> ${new Date(output.read_at || undefined)}
190 <div class="scrollable" id="feed-content">${output.content}</div>
192 return output.content;
195 apiDelete('/feeds/:feed_id', async (req, res) => {
196 const id = req.params.feed_id;
198 query.deleteFeed.run(id);
199 res.setHeader('HX-Trigger', 'newFeed');
203 async function periodicIngest() {
205 setTimeout(periodicIngest, 1000 * 60 * 10);
210 app.listen(process.env.PORT || 8000, () => {
211 console.log('Listening on port', process.env.PORT || 8000);