-
Notifications
You must be signed in to change notification settings - Fork 78
/
Copy pathFeed.js
221 lines (199 loc) · 5.98 KB
/
Feed.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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
'use strict';
const FeedParser = require('feedparser');
const request = require('request');
const FeedError = require('./FeedError');
const FeedItem = require('./FeedItem'); // eslint-disable-line no-unused-vars
/**
* Map of specially handled error codes
* @type {Object}
*/
const RESPONSE_CODES = {
OK: 200,
NOT_FOUND: 404,
ISE: 500,
};
/**
* This module manages automatically how many feed items
* it will keep in memory, and basically it will have a
* maximum history which is how many items the feed has
* multiplied by this number below. So, if the feed have
* 10 items, we will keep 30 items max in the history.
* @type {Number}
*/
const historyLengthMultiplier = 3;
/**
* Default UserAgent string
* Since static stuff doesn't work in older versions, keep using global const
* @type {String}
*/
const DEFAULT_UA = 'Node/RssFeedEmitter (https://github.com/filipedeschamps/rss-feed-emitter)';
/**
* Allowed mime types to allow fetching
* @type {Array<string>}
*/
const ALLOWED_MIMES = ['text/html', 'application/xhtml+xml', 'application/xml', 'text/xml'];
/**
* Storage object for properties of a feed
* @class
* @property {string} url Feed url
* @property {FeedItem[]} items items currently retrieved from the feed
* @property {number} refresh timeout between refreshes
* @property {string} userAgent User Agent string to fetch the feed with
* @property {string} eventName event name to use when emitting this feed
*/
class Feed {
/**
* Create a feed
* @param {Feed} data object with feed data
*/
constructor(data) {
/**
* Array of item
* @type {FeedItem[]}
*/
this.items; // eslint-disable-line no-unused-expressions
/**
* Feed url for retrieving feed items
* @type {string}
*/
this.url; // eslint-disable-line no-unused-expressions
/**
* Duration between feed refreshes
* @type {number}
*/
this.refresh; // eslint-disable-line no-unused-expressions
/**
* If the user has specified a User Agent
* we will use that as the 'user-agent' header when
* making requests, otherwise we use the default option.
* @type {string}
*/
this.userAgent; // eslint-disable-line no-unused-expressions
/**
* event name for this feed to emit when a new item becomes available
* @type {String}
*/
this.eventName; // eslint-disable-line no-unused-expressions
/**
* Maximum history length
* @type {number}
*/
this.maxHistoryLength; // eslint-disable-line no-unused-expressions
({
items: this.items, url: this.url, refresh: this.refresh, userAgent: this.userAgent,
eventName: this.eventName,
} = data);
if (!this.items) this.items = [];
if (!this.url) throw new TypeError('missing required field `url`');
if (!this.refresh) this.refresh = 60000;
if (!this.userAgent) this.userAgent = DEFAULT_UA;
if (!this.eventName) this.eventName = 'new-item';
}
/**
* Given a feed and item, try to find
* it inside the feed item list. We will use
* this to see if there's already an item inside
* the feed item list. If there is, we know it's
* not a new item.
* @public
* @param {FeedItem} item item specitics
* @returns {FeedItem} the matched element
*/
findItem(item) {
return this.items.find((entry) => {
// if feed is RSS 2.x, check existence of 'guid'
if (item.guid) return entry.guid === item.guid;
// if feed is Atom 1.x, check existence of 'id'
if (item.id) return entry.id === item.id;
// default object with 'link' and 'title'
return entry.link === item.link && entry.title === item.title;
});
}
/**
* Update the maximum history length based on the length of a feed retrieval
* @public
* @param {FeedItem[]} newItems new list of items to base the history length on
*/
updateHxLength(newItems) {
this.maxHistoryLength = newItems.length * historyLengthMultiplier;
}
/**
* Add an item to the feed
* @public
* @param {FeedItem} item Feed item. Indeterminant structure.
*/
addItem(item) {
this.items.push(item);
this.items = this.items.slice(this.items.length - this.maxHistoryLength, this.items.length);
}
/**
* Fetch the data for this feed
* @public
* @returns {Promise} array of new feed items
*/
fetchData() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const items = [];
const feedparser = new FeedParser();
feedparser.on('readable', () => {
const item = feedparser.read();
item.meta.link = this.url;
items.push(item);
});
feedparser.on('error', () => {
this.handleError(new FeedError(`Cannot parse ${this.url} XML`, 'invalid_feed', this.url));
});
feedparser.on('end', () => {
resolve(items);
});
this.get(feedparser);
});
}
/**
* Perform the feed parsing
* @private
* @param {FeedParser} feedparser feedparser instance to use for parsing a retrieved feed
*/
get(feedparser) {
request
.get({
url: this.url,
headers: {
'user-agent': this.userAgent,
accept: ALLOWED_MIMES.join(','),
},
})
.on('response', (res) => {
if (res.statusCode !== RESPONSE_CODES.OK) {
this.handleError(new FeedError(`This URL returned a ${res.statusCode} status code`, 'fetch_url_error', this.url));
}
})
.on('error', () => {
this.handleError(new FeedError(`Cannot connect to ${this.url}`, 'fetch_url_error', this.url));
})
.pipe(feedparser)
.on('end', () => {});
}
/**
* Handle errors inside the feed retrieval process
* @param {Error} error error to be handled
* @private
*/
handleError(error) {
if (this.handler) {
this.handler.handle(error);
} else {
throw error;
}
}
/**
* Destroy feed
* @public
*/
destroy() {
clearInterval(this.interval);
delete this.interval;
}
}
module.exports = Feed;