-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbackend.js
102 lines (99 loc) · 3.14 KB
/
backend.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import fs from 'fs/promises';
import http from 'http';
import rssToJson from 'rss-to-json';
class Server {
constructor() {
this.init().catch(console.log);
}
async init() {
await this.updateConfig();
this._http = http.createServer(async (req, res) => {
try {
await this.handleRequest(req, res);
} catch(e) {
return this.sendResponse(res, { statusCode: e.statusCode || 500, data: { message: e.message } });
}
});
this._http.listen(this.config.port);
console.log(`Server started listening on ${this.config.port}`);
}
parseQuery(req) {
const queryString = req.url.split('?')[1] ?? '';
const query = {};
queryString.split('&').forEach(pair => {
const [key, value] = pair.split('=');
query[key] = value;
});
if(!query.feeds || !this.config.feeds[query.feeds]) {
query.feeds = 'default';
}
return query;
}
async handleRequest(req, res) {
const query = this.parseQuery(req);
if(req.method === 'GET' && req.url.startsWith('/api/config')) {
return this.sendResponse(res, { data: this.config });
}
if(req.method === 'GET' && req.url.startsWith('/api/news')) {
return this.sendResponse(res, { data: await this.getRSS(query) });
}
await this.serveStatic(req, res);
}
async updateConfig() {
this.config = JSON.parse(await fs.readFile('./config.json'))
}
async getRSS(query) {
await this.updateConfig();
const results = [];
await Promise.allSettled(this.config.feeds[query.feeds].map(f => {
return new Promise(async (resolve, reject) => {
const t = setTimeout(() => {
console.log(`RSS load timeout for ${f.feed}`);
reject();
}, this.config.timeout);
try {
const d = await rssToJson.parse(f.feed);
clearTimeout(t);
if(d.items) results.push(...d.items.map(i => Object.assign(i, { feed: f.name ?? this.generateTitle(d), type: f.type ?? 'news' })));
} catch(e) {
console.log(`Failed to parse ${f.feed}`, e.errno);
console.log(e);
}
resolve();
});
}));
return results
.sort((a, b) => {
if(a.created < b.created) return 1;
if(a.created > b.created) return -1;
return 0;
})
.slice(0, 100);
}
generateTitle(data) {
return data.title.split(/[^A-Za-z0-9\s]/)[0].trim();
}
async serveStatic(req, res) {
const [url] = req.url.split('?');
const filePath = url === '/' ? '/index.html' : url;
const fileExt = filePath.slice(filePath.lastIndexOf('.')+1);
this.sendResponse(res, { contentType: this.extToMime(fileExt), data: await fs.readFile(`./public${filePath}`) });
}
sendResponse(res, { statusCode=200, contentType='application/json', data }) {
res.writeHead(statusCode, { 'Content-Type': contentType });
res.end(contentType === 'application/json' ? JSON.stringify(data) : data, 'utf-8');
}
extToMime(ext) {
switch(ext) {
case 'html':
case 'css':
return `text/${ext}`;
case 'js':
return `text/javascript`;
case 'ico':
case 'png':
return `image/${ext}`;
}
}
}
new Server();