-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
304 lines (255 loc) · 10.5 KB
/
index.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
const request = require('request'); // https://github.com/request/request
/**
* @class AdvancedRequest
*
* Manager for sending HTTP requests with multiple tries, wait intervals, and timeouts
* Uses node module 'request'
*
* pass in format of:
* {
* 'url': url,
* 'method': "POST", // or "GET"
* 'callback': callback,
* 'postData': {asdfa: "asdf", "okef": "efe" }
* }
*/
/**
* lastRunHash
*
* This can be configured by adding names as keys and values with requiredInterval
* in order to WAIT before sending another request of that name. If you wanted to
* only send one request with name DevAPI_SendFriendRequest per minute, set the
* requiredInterval to 1000*60 and the minute will automatically be enforced
*
* All 'lastruns' start with the moment of script start
* to make sure script restarts don't accidentally disobey the interval
*
*/
let lastRunHash = {
//'WebAPIfollowCMD': { lastReqTime: new Date().getTime(), requiredInterval: 1000*10 }, // 10 seconds
//'WebAPIunfollowCMD': { lastReqTime: new Date().getTime(), requiredInterval: 1000*60*10 }, // 10 minutes
};
class AdvancedRequest {
constructor (args) {
this.opts = args;
this.opts.method = args.method || "GET";
// stringify if necessary
if (typeof(this.opts.postData) == "object") {
this.opts.postData = JSON.stringify(this.opts.postData);
}
this.name = args.name || "unnamed request";
this.maxRetries = args.maxRetries || 10; // Pass in 0 for unlimited
// Pass this in as true to avoid multipart header. This is rare; some API endpoints need it
this.noMultipartHeader = args.noMultipartHeader || false;
// If downloading file to disk, pass in this as the raw filename to save as
this.saveAs = args.saveAs;
this.isBinaryRequest = args.isBinaryRequest || this.saveAs;
this.callback = args.callback || function () {
console.log(`[!] No request callback provided! req name: ${this.name}`);
};
this.numTriesSoFar = 0; // start at zero
this.data = ''; // Request data
this.apiObj = args.apiObj || {};
this.requestHeaders = {}; // nodejs request sent OUT
this.responseHeaders = []; // headers for the request RESPONSE
this.isRequestComplete = false;
this.markedToCancel = false;
}
/**
* addHeader
* Provide the FULL header. ex:
* addHeader('Cookie: awefaf=ewfef;wefweafwef');
*/
addHeader (fullHeader) {
let pieces = fullHeader.split(': ');
// .pop to pull off the first instance of ': ' because ': ' could occur later in the header
this.requestHeaders[pieces.shift()] = pieces.join(': '); // Could do .toLowerCase() on pieces.shift()
}
getResponseHeaders () {
return this.responseHeaders;
}
/**
* onRequestRetriesExhausted
*
* Occurs when the request has called .fail enough times to equal the 'this.maxRetries' count
* You may want to override this function as your needs demand
*/
onRequestRetriesExhausted () {
console.log(`[!] Max request retries exceeded for request named (${this.name}). Throwing exception!`);
throw "ADVANCEDREQUEST_RETRIES_EXCEEDED";
}
/**
* fail
*
* Increment retries counter and retry the request in 'sleepSeconds' seconds
* or KILL the node process if too many retries have happened
*/
fail (sleepSeconds, additionalMsg) {
this.numTriesSoFar++;
console.log("[!]", additionalMsg || 'AdvancedRequest.fail call -',
"Status:", this.responseStatusCode, "url:", this.opts.url, "name:", this.name,
"tries left:", this.maxRetries - this.numTriesSoFar, "options:", this.reqOptions);
if (this.numTriesSoFar >= this.maxRetries && this.maxRetries != 0) {
return this.onRequestRetriesExhausted();
}
setTimeout(this.run.bind(this), sleepSeconds * 1000); // convert to full seconds here
}
getLastRunHash () {
// Allow setting a lastRunHash on the class (probably subclass) or use the module one instead
return this.lastRunHash || lastRunHash;
}
onFinish (result) {
// Update last run time for this request if applicable. (in future add username?)
if (this.getLastRunHash()[this.name]) {
// We use THIS moment, the END of the request as the marker.
this.getLastRunHash()[this.name].lastReqTime = new Date().getTime();
}
this.isRequestComplete = true;
return this.callback(result);
}
/**
* postProcess
*
* Recommended that this be overridden to check for particular error types
*/
postProcess () {
console.log("[D] NOTE: AdvancedRequest.postProcess probably should be overridden " +
"(usually through inheritance) and given verification criteria");
// for example, override and have a check:
// if (!this.data) return this.fail(10, "Response was blank! Retrying in 10 seconds");
// In your subclass, remember to include this line to complete the request
this.onFinish(this.data);
}
isSleepIntervalNecessary () {
// If this is a request with limitations
if (this.getLastRunHash()[this.name]) {
let now = new Date().getTime();
// initialize now if not initialized yet
if (!this.getLastRunHash()[this.name].lastReqTime) {
this.getLastRunHash()[this.name].lastReqTime = now;
}
let millisecondsSinceLastRequest = (now - this.getLastRunHash()[this.name].lastReqTime);
return millisecondsSinceLastRequest < this.getLastRunHash()[this.name].requiredInterval;
}
return false;
}
sleepIntervalIfNecessary (callback) {
if (this.isSleepIntervalNecessary()) {
let millisecondsSinceLastRequest = (new Date().getTime() - this.getLastRunHash()[this.name].lastReqTime);
let timeToSleep = this.getLastRunHash()[this.name].requiredInterval - millisecondsSinceLastRequest;
console.log(`[D] AdvancedRequest.${this.name} - Sleeping ${timeToSleep/(1000)} seconds now`);
setTimeout(() => { this.sleepIntervalIfNecessary(callback); }, timeToSleep);
} else {
return callback();
}
}
/**
* cancelRequest
* Call at any point to cancel a request from being sent out or stop it from
* retrying if it is in the process of failing or sleeping.
* NOTE: The provided callback in the constructor will not be called if canceled.
*/
cancelRequest () {
if (this.markedToCancel) {
return console.log("[D] AdvancedRequest - This request is already canceled!");
} else if (this.isRequestComplete) {
return console.log("[!] AdvancedRequest - Warning! Trying to cancel a request that already completed");
} else {
this.reqObj && this.reqObj.abort && this.reqObj.abort(); // abort request if in progress
clearTimeout(this._requestTimeoutInt);
this.markedToCancel = true;
}
}
/**
* run - but compatible with async/await. Both run and runAsync are actually asynchronous
* Can be used like so:
* let requestData = await new AdvancedRequest({...}).runAsync()
*/
async runAsync () {
// this promise will return the request data,
return await new Promise((resolve, reject) => {
this.callback = resolve; // change callback to resolve, called at end of request, in onFinish
return this.run(); // fire off request
});
}
/**
* run
* Actually perform request.
* DEPRECATED style to fire request, though will be left in for backwards compatibility
*/
run () {
if (this.markedToCancel) return console.log(`[D] ${this.name} - Request canceled.`); // bail out right now
if (this.isSleepIntervalNecessary()) {
return this.sleepIntervalIfNecessary(() => { this.run.apply(this, arguments); });
}
let extraOpts = {
headers: this.requestHeaders,
gzip: true,
timeout: 60 * 1000, // number of ms to wait for response headers (1 min)
// https://github.com/nodejs/node/issues/3692 UGGHHHH this bug results in
// a call to this.fail(10) then next request works
//agentOptions: { ciphers: 'ALL', secureProtocol: 'TLSv1_method', },
};
// If we're saving, we want an image file and therefore want the BUFFER
// received, instead of the default "string". To maybe sha1 it, etc
if (this.isBinaryRequest) {
extraOpts["encoding"] = null; // set to null to get binary and not string
}
if (this.opts.postData) {
if (this.noMultipartHeader) {
extraOpts["form"] = this.opts.postData;
} else {
extraOpts["multipart"] = [ {
//'Content-Type': 'application/x-www-form-urlencoded',
body: (typeof(this.opts.postData) == "string") ? this.opts.postData : JSON.stringify(this.opts.postData),
} ];
}
}
// Merge options to pass on additional options to request
this.reqOptions = Object.assign({}, this.opts, extraOpts);
// We're using 'form' or 'multipart' not the 'postData' key
if ('postData' in this.reqOptions) {
delete this.reqOptions['postData'];
}
// Set a protection timeout because node's request module SOMETIMES doesn't timeout properly. (v2.88.0 at least)
this._requestTimeoutInt = setTimeout(() => {
this.reqObj.abort();
this.fail(.1, `[!] ${this.name || "AdvancedRequest"} - Timeout for request (within request module). Retrying.`);
}, extraOpts.timeout + 2000); // 2 seconds above established timeout
this.reqObj = request(this.reqOptions, (error, response, body) => {
clearTimeout(this._requestTimeoutInt);
if (error) {
// this.fail helps for bugs WITH NO KNOWN FIX LIKE: routines:SSL3_GET_RECORD:wrong version number:
return this.fail(10, `${this.name} - ERROR with advanced request code somehow!! Err: ${error}`);
} else {
// body will be Buffer if isBinaryRequest or string otherwise
this.data = body;
this.responseStatusCode = (response) ? response.statusCode : -1;
this.responseHeaders = (response) ? response.headers : [];
// This will call the subclassed 'postProcess' if this class has been extended
return this.postProcess();
}
});
// Quickly attach pipe call before tick ends. Next tick will be request call!
if (this.saveAs) {
this.reqObj.pipe(fs.createWriteStream(this.saveAs));
}
}
};
module.exports = {
AdvancedRequest: AdvancedRequest,
setLastRunHash: function (newLastRunHash) {
lastRunHash = newLastRunHash;
},
addToLastRunHash: function (items) {
// Merge options to pass on additional options to request
lastRunHash = Object.assign({}, lastRunHash, items);
},
removeFromLastRunHash: function (items) {
for (let i in items) {
if (i in lastRunHash) {
delete lastRunHash[i];
}
}
},
};