mirror of
https://github.com/thewesker/bug-em.git
synced 2025-12-22 21:11:06 -05:00
lol
This commit is contained in:
11
node_modules/twit/lib/endpoints.js
generated
vendored
Normal file
11
node_modules/twit/lib/endpoints.js
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Twitter Endpoints
|
||||
module.exports = {
|
||||
API_HOST : 'https://api.twitter.com/'
|
||||
, REST_ROOT : 'https://api.twitter.com/1.1/'
|
||||
, PUB_STREAM : 'https://stream.twitter.com/1.1/'
|
||||
, USER_STREAM : 'https://userstream.twitter.com/1.1/'
|
||||
, SITE_STREAM : 'https://sitestream.twitter.com/1.1/'
|
||||
, MEDIA_UPLOAD : 'https://upload.twitter.com/1.1/'
|
||||
, OA_REQ : 'https://api.twitter.com/oauth/request_token'
|
||||
, OA_ACCESS : 'https://api.twitter.com/oauth/access_token'
|
||||
}
|
||||
143
node_modules/twit/lib/file_uploader.js
generated
vendored
Normal file
143
node_modules/twit/lib/file_uploader.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var mime = require('mime');
|
||||
var util = require('util');
|
||||
|
||||
var MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
||||
var MAX_FILE_CHUNK_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* FileUploader class used to upload a file to twitter via the /media/upload (chunked) API.
|
||||
* Usage:
|
||||
* var fu = new FileUploader({ file_path: '/foo/bar/baz.mp4' }, twit);
|
||||
* fu.upload(function (err, bodyObj, resp) {
|
||||
* console.log(err, bodyObj);
|
||||
* })
|
||||
*
|
||||
* @param {Object} params Object of the form { file_path: String }.
|
||||
* @param {Twit(object)} twit Twit instance.
|
||||
*/
|
||||
var FileUploader = function (params, twit) {
|
||||
assert(params)
|
||||
assert(params.file_path, 'Must specify `file_path` to upload a file. Got: ' + params.file_path + '.')
|
||||
var self = this;
|
||||
self._file_path = params.file_path;
|
||||
self._twit = twit;
|
||||
self._isUploading = false;
|
||||
self._isFileStreamEnded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Twitter via the /media/upload (chunked) API.
|
||||
*
|
||||
* @param {Function} cb function (err, data, resp)
|
||||
*/
|
||||
FileUploader.prototype.upload = function (cb) {
|
||||
var self = this;
|
||||
|
||||
// Send INIT command with file info and get back a media_id_string we can use to APPEND chunks to it.
|
||||
self._initMedia(function (err, bodyObj, resp) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
} else {
|
||||
var mediaTmpId = bodyObj.media_id_string;
|
||||
var chunkNumber = 0;
|
||||
var mediaFile = fs.createReadStream(self._file_path, { highWatermark: MAX_FILE_CHUNK_BYTES });
|
||||
|
||||
mediaFile.on('data', function (chunk) {
|
||||
// Pause our file stream from emitting `data` events until the upload of this chunk completes.
|
||||
// Any data that becomes available will remain in the internal buffer.
|
||||
mediaFile.pause();
|
||||
self._isUploading = true;
|
||||
|
||||
self._appendMedia(mediaTmpId, chunk.toString('base64'), chunkNumber, function (err, bodyObj, resp) {
|
||||
self._isUploading = false;
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
if (self._isUploadComplete()) {
|
||||
// We've hit the end of our stream; send FINALIZE command.
|
||||
self._finalizeMedia(mediaTmpId, cb);
|
||||
} else {
|
||||
// Tell our file stream to start emitting `data` events again.
|
||||
chunkNumber++;
|
||||
mediaFile.resume();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mediaFile.on('end', function () {
|
||||
// Mark our file streaming complete, and if done, send FINALIZE command.
|
||||
self._isFileStreamEnded = true;
|
||||
if (self._isUploadComplete()) {
|
||||
self._finalizeMedia(mediaTmpId, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
FileUploader.prototype._isUploadComplete = function () {
|
||||
return !this._isUploading && this._isFileStreamEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send FINALIZE command for media object with id `media_id`.
|
||||
*
|
||||
* @param {String} media_id
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._finalizeMedia = function(media_id, cb) {
|
||||
var self = this;
|
||||
self._twit.post('media/upload', {
|
||||
command: 'FINALIZE',
|
||||
media_id: media_id
|
||||
}, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send APPEND command for media object with id `media_id`.
|
||||
* Append the chunk to the media object, then resume streaming our mediaFile.
|
||||
*
|
||||
* @param {String} media_id media_id_string received from Twitter after sending INIT comand.
|
||||
* @param {String} chunk_part Base64-encoded String chunk of the media file.
|
||||
* @param {Number} segment_index Index of the segment.
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._appendMedia = function(media_id_string, chunk_part, segment_index, cb) {
|
||||
var self = this;
|
||||
self._twit.post('media/upload', {
|
||||
command: 'APPEND',
|
||||
media_id: media_id_string.toString(),
|
||||
segment_index: segment_index,
|
||||
media: chunk_part,
|
||||
}, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send INIT command for our underlying media object.
|
||||
*
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._initMedia = function (cb) {
|
||||
var self = this;
|
||||
var mediaType = mime.lookup(self._file_path);
|
||||
var mediaFileSizeBytes = fs.statSync(self._file_path).size;
|
||||
|
||||
// Check the file size - it should not go over 15MB for video.
|
||||
// See https://dev.twitter.com/rest/reference/post/media/upload-chunked
|
||||
if (mediaFileSizeBytes < MAX_FILE_SIZE_BYTES) {
|
||||
self._twit.post('media/upload', {
|
||||
'command': 'INIT',
|
||||
'media_type': mediaType,
|
||||
'total_bytes': mediaFileSizeBytes
|
||||
}, cb);
|
||||
} else {
|
||||
var errMsg = util.format('This file is too large. Max size is %dB. Got: %dB.', MAX_FILE_SIZE_BYTES, mediaFileSizeBytes);
|
||||
cb(new Error(errMsg));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileUploader
|
||||
128
node_modules/twit/lib/helpers.js
generated
vendored
Normal file
128
node_modules/twit/lib/helpers.js
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
var querystring = require('querystring');
|
||||
var request = require('request');
|
||||
|
||||
var endpoints = require('./endpoints');
|
||||
|
||||
/**
|
||||
* Encodes object as a querystring, to be used as the suffix of request URLs.
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
*/
|
||||
exports.makeQueryString = function (obj) {
|
||||
var qs = querystring.stringify(obj)
|
||||
qs = qs.replace(/\!/g, "%21")
|
||||
.replace(/\'/g, "%27")
|
||||
.replace(/\(/g, "%28")
|
||||
.replace(/\)/g, "%29")
|
||||
.replace(/\*/g, "%2A");
|
||||
return qs
|
||||
}
|
||||
|
||||
/**
|
||||
* For each `/:param` fragment in path, move the value in params
|
||||
* at that key to path. If the key is not found in params, throw.
|
||||
* Modifies both params and path values.
|
||||
*
|
||||
* @param {Objet} params Object used to build path.
|
||||
* @param {String} path String to transform.
|
||||
* @return {Undefined}
|
||||
*
|
||||
*/
|
||||
exports.moveParamsIntoPath = function (params, path) {
|
||||
var rgxParam = /\/:(\w+)/g
|
||||
var missingParamErr = null
|
||||
|
||||
path = path.replace(rgxParam, function (hit) {
|
||||
var paramName = hit.slice(2)
|
||||
var suppliedVal = params[paramName]
|
||||
if (!suppliedVal) {
|
||||
throw new Error('Twit: Params object is missing a required parameter for this request: `'+paramName+'`')
|
||||
}
|
||||
var retVal = '/' + suppliedVal
|
||||
delete params[paramName]
|
||||
return retVal
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* When Twitter returns a response that looks like an error response,
|
||||
* use this function to attach the error info in the response body to `err`.
|
||||
*
|
||||
* @param {Error} err Error instance to which body info will be attached
|
||||
* @param {Object} body JSON object that is the deserialized HTTP response body received from Twitter
|
||||
* @return {Undefined}
|
||||
*/
|
||||
exports.attachBodyInfoToError = function (err, body) {
|
||||
err.twitterReply = body;
|
||||
if (!body) {
|
||||
return
|
||||
}
|
||||
if (body.error) {
|
||||
// the body itself is an error object
|
||||
err.message = body.error
|
||||
err.allErrors = err.allErrors.concat([body])
|
||||
} else if (body.errors && body.errors.length) {
|
||||
// body contains multiple error objects
|
||||
err.message = body.errors[0].message;
|
||||
err.code = body.errors[0].code;
|
||||
err.allErrors = err.allErrors.concat(body.errors)
|
||||
}
|
||||
}
|
||||
|
||||
exports.makeTwitError = function (message) {
|
||||
var err = new Error()
|
||||
if (message) {
|
||||
err.message = message
|
||||
}
|
||||
err.code = null
|
||||
err.allErrors = []
|
||||
err.twitterReply = null
|
||||
return err
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bearer token for OAuth2
|
||||
* @param {String} consumer_key
|
||||
* @param {String} consumer_secret
|
||||
* @param {Function} cb
|
||||
*
|
||||
* Calls `cb` with Error, String
|
||||
*
|
||||
* Error (if it exists) is guaranteed to be Twit error-formatted.
|
||||
* String (if it exists) is the bearer token received from Twitter.
|
||||
*/
|
||||
exports.getBearerToken = function (consumer_key, consumer_secret, cb) {
|
||||
// use OAuth 2 for app-only auth (Twitter requires this)
|
||||
// get a bearer token using our app's credentials
|
||||
var b64Credentials = new Buffer(consumer_key + ':' + consumer_secret).toString('base64');
|
||||
request.post({
|
||||
url: endpoints.API_HOST + '/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + b64Credentials,
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
json: true,
|
||||
}, function (err, res, body) {
|
||||
if (err) {
|
||||
var error = exports.makeTwitError(err.toString());
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
if ( !body ) {
|
||||
var error = exports.makeTwitError('Not valid reply from Twitter upon obtaining bearer token');
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
if (body.token_type !== 'bearer') {
|
||||
var error = exports.makeTwitError('Unexpected reply from Twitter upon obtaining bearer token');
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
return cb(err, body.access_token);
|
||||
})
|
||||
}
|
||||
56
node_modules/twit/lib/parser.js
generated
vendored
Normal file
56
node_modules/twit/lib/parser.js
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Parser - for Twitter Streaming API
|
||||
//
|
||||
var util = require('util')
|
||||
, EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var Parser = module.exports = function () {
|
||||
this.message = ''
|
||||
|
||||
EventEmitter.call(this);
|
||||
};
|
||||
|
||||
util.inherits(Parser, EventEmitter);
|
||||
|
||||
Parser.prototype.parse = function (chunk) {
|
||||
this.message += chunk;
|
||||
chunk = this.message;
|
||||
|
||||
var size = chunk.length
|
||||
, start = 0
|
||||
, offset = 0
|
||||
, curr
|
||||
, next;
|
||||
|
||||
while (offset < size) {
|
||||
curr = chunk[offset];
|
||||
next = chunk[offset + 1];
|
||||
|
||||
if (curr === '\r' && next === '\n') {
|
||||
var piece = chunk.slice(start, offset);
|
||||
start = offset += 2;
|
||||
|
||||
if (!piece.length) { continue; } //empty object
|
||||
|
||||
if (piece === 'Exceeded connection limit for user') {
|
||||
this.emit('connection-limit-exceeded',
|
||||
new Error('Twitter says: ' + piece + '. Only instantiate one stream per set of credentials.'));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var msg = JSON.parse(piece)
|
||||
} catch (err) {
|
||||
this.emit('error', new Error('Error parsing twitter reply: `'+piece+'`, error message `'+err+'`'));
|
||||
} finally {
|
||||
if (msg)
|
||||
this.emit('element', msg)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
this.message = chunk.slice(start, size);
|
||||
};
|
||||
2
node_modules/twit/lib/settings.js
generated
vendored
Normal file
2
node_modules/twit/lib/settings.js
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// set of status codes where we don't attempt reconnecting to Twitter
|
||||
exports.STATUS_CODES_TO_ABORT_ON = [ 400, 401, 403, 404, 406, 410, 422 ];
|
||||
358
node_modules/twit/lib/streaming-api-connection.js
generated
vendored
Normal file
358
node_modules/twit/lib/streaming-api-connection.js
generated
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
|
||||
var helpers = require('./helpers')
|
||||
var Parser = require('./parser');
|
||||
var request = require('request');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON
|
||||
|
||||
var StreamingAPIConnection = function (reqOpts, twitOptions) {
|
||||
this.reqOpts = reqOpts
|
||||
this.twitOptions = twitOptions
|
||||
this._twitter_time_minus_local_time_ms = 0
|
||||
EventEmitter.call(this)
|
||||
}
|
||||
|
||||
util.inherits(StreamingAPIConnection, EventEmitter)
|
||||
|
||||
/**
|
||||
* Resets the connection.
|
||||
* - clears request, response, parser
|
||||
* - removes scheduled reconnect handle (if one was scheduled)
|
||||
* - stops the stall abort timeout handle (if one was scheduled)
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetConnection = function () {
|
||||
if (this.request) {
|
||||
// clear our reference to the `request` instance
|
||||
this.request.removeAllListeners();
|
||||
this.request.destroy();
|
||||
}
|
||||
|
||||
if (this.response) {
|
||||
// clear our reference to the http.IncomingMessage instance
|
||||
this.response.removeAllListeners();
|
||||
this.response.destroy();
|
||||
}
|
||||
|
||||
if (this.parser) {
|
||||
this.parser.removeAllListeners()
|
||||
}
|
||||
|
||||
// ensure a scheduled reconnect does not occur (if one was scheduled)
|
||||
// this can happen if we get a close event before .stop() is called
|
||||
clearTimeout(this._scheduledReconnect)
|
||||
delete this._scheduledReconnect
|
||||
|
||||
// clear our stall abort timeout
|
||||
this._stopStallAbortTimeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the parameters used in determining the next reconnect time
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetRetryParams = function () {
|
||||
// delay for next reconnection attempt
|
||||
this._connectInterval = 0
|
||||
// flag indicating whether we used a 0-delay reconnect
|
||||
this._usedFirstReconnect = false
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._startPersistentConnection = function () {
|
||||
var self = this;
|
||||
self._resetConnection();
|
||||
self._setupParser();
|
||||
self._resetStallAbortTimeout();
|
||||
self._setOauthTimestamp();
|
||||
self.request = request.post(this.reqOpts);
|
||||
self.emit('connect', self.request);
|
||||
self.request.on('response', function (response) {
|
||||
self._updateOauthTimestampOffsetFromResponse(response)
|
||||
// reset our reconnection attempt flag so next attempt goes through with 0 delay
|
||||
// if we get a transport-level error
|
||||
self._usedFirstReconnect = false;
|
||||
// start a stall abort timeout handle
|
||||
self._resetStallAbortTimeout();
|
||||
self.response = response
|
||||
if (STATUS_CODES_TO_ABORT_ON.indexOf(self.response.statusCode) !== -1) {
|
||||
// We got a status code telling us we should abort the connection.
|
||||
// Read the body from the response and return an error to the user.
|
||||
var body = '';
|
||||
var compressedBody = '';
|
||||
|
||||
self.response.on('data', function (chunk) {
|
||||
compressedBody += chunk.toString('utf8');
|
||||
})
|
||||
|
||||
var gunzip = zlib.createGunzip();
|
||||
self.response.pipe(gunzip);
|
||||
gunzip.on('data', function (chunk) {
|
||||
body += chunk.toString('utf8')
|
||||
})
|
||||
|
||||
gunzip.on('end', function () {
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (jsonDecodeError) {
|
||||
// Twitter may send an HTML body
|
||||
// if non-JSON text was returned, we'll just attach it to the error as-is
|
||||
}
|
||||
// surface the error to the user
|
||||
var error = helpers.makeTwitError('Bad Twitter streaming request: ' + self.response.statusCode)
|
||||
error.statusCode = response ? response.statusCode: null;
|
||||
helpers.attachBodyInfoToError(error, body)
|
||||
self.emit('error', error);
|
||||
// stop the stream explicitly so we don't reconnect
|
||||
self.stop()
|
||||
body = null;
|
||||
});
|
||||
gunzip.on('error', function (err) {
|
||||
// If Twitter sends us back an uncompressed HTTP response, gzip will error out.
|
||||
// Handle this by emitting an error with the uncompressed response body.
|
||||
var errMsg = 'Gzip error: ' + err.message;
|
||||
var twitErr = helpers.makeTwitError(errMsg);
|
||||
twitErr.statusCode = self.response.statusCode;
|
||||
helpers.attachBodyInfoToError(twitErr, compressedBody);
|
||||
self.emit('parser-error', twitErr);
|
||||
});
|
||||
} else if (self.response.statusCode === 420) {
|
||||
// close the connection forcibly so a reconnect is scheduled by `self.onClose()`
|
||||
self._scheduleReconnect();
|
||||
} else {
|
||||
// We got an OK status code - the response should be valid.
|
||||
// Read the body from the response and return to the user.
|
||||
var gunzip = zlib.createGunzip();
|
||||
self.response.pipe(gunzip);
|
||||
|
||||
//pass all response data to parser
|
||||
gunzip.on('data', function (chunk) {
|
||||
self._connectInterval = 0
|
||||
// stop stall timer, and start a new one
|
||||
self._resetStallAbortTimeout();
|
||||
self.parser.parse(chunk.toString('utf8'));
|
||||
});
|
||||
|
||||
gunzip.on('close', self._onClose.bind(self))
|
||||
gunzip.on('error', function (err) {
|
||||
self.emit('error', err);
|
||||
})
|
||||
self.response.on('error', function (err) {
|
||||
// expose response errors on twit instance
|
||||
self.emit('error', err);
|
||||
})
|
||||
|
||||
// connected without an error response from Twitter, emit `connected` event
|
||||
// this must be emitted after all its event handlers are bound
|
||||
// so the reference to `self.response` is not interfered-with by the user until it is emitted
|
||||
self.emit('connected', self.response);
|
||||
}
|
||||
});
|
||||
self.request.on('close', self._onClose.bind(self));
|
||||
self.request.on('error', function (err) { self._scheduleReconnect.bind(self) });
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the request or response closes.
|
||||
* Schedule a reconnect according to Twitter's reconnect guidelines
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._onClose = function () {
|
||||
var self = this;
|
||||
self._stopStallAbortTimeout();
|
||||
if (self._scheduledReconnect) {
|
||||
// if we already have a reconnect scheduled, don't schedule another one.
|
||||
// this race condition can happen if the http.ClientRequest and http.IncomingMessage both emit `close`
|
||||
return
|
||||
}
|
||||
|
||||
self._scheduleReconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off the http request, and persist the connection
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype.start = function () {
|
||||
this._resetRetryParams();
|
||||
this._startPersistentConnection();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the http request, stop scheduled reconnect (if one was scheduled) and clear state
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype.stop = function () {
|
||||
// clear connection variables and timeout handles
|
||||
this._resetConnection();
|
||||
this._resetRetryParams();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and restart the stall abort timer (called when new data is received)
|
||||
*
|
||||
* If we go 90s without receiving data from twitter, we abort the request & reconnect.
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetStallAbortTimeout = function () {
|
||||
var self = this;
|
||||
// stop the previous stall abort timer
|
||||
self._stopStallAbortTimeout();
|
||||
//start a new 90s timeout to trigger a close & reconnect if no data received
|
||||
self._stallAbortTimeout = setTimeout(function () {
|
||||
self._scheduleReconnect()
|
||||
}, 90000);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop stall timeout
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._stopStallAbortTimeout = function () {
|
||||
clearTimeout(this._stallAbortTimeout);
|
||||
// mark the timer as `null` so it is clear via introspection that the timeout is not scheduled
|
||||
delete this._stallAbortTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the next time a reconnect should occur (based on the last HTTP response received)
|
||||
* and starts a timeout handle to begin reconnecting after `self._connectInterval` passes.
|
||||
*
|
||||
* @return {Undefined}
|
||||
*/
|
||||
StreamingAPIConnection.prototype._scheduleReconnect = function () {
|
||||
var self = this;
|
||||
if (self.response && self.response.statusCode === 420) {
|
||||
// we are being rate limited
|
||||
// start with a 1 minute wait and double each attempt
|
||||
if (!self._connectInterval) {
|
||||
self._connectInterval = 60000;
|
||||
} else {
|
||||
self._connectInterval *= 2;
|
||||
}
|
||||
} else if (self.response && String(self.response.statusCode).charAt(0) === '5') {
|
||||
// twitter 5xx errors
|
||||
// start with a 5s wait, double each attempt up to 320s
|
||||
if (!self._connectInterval) {
|
||||
self._connectInterval = 5000;
|
||||
} else if (self._connectInterval < 320000) {
|
||||
self._connectInterval *= 2;
|
||||
} else {
|
||||
self._connectInterval = 320000;
|
||||
}
|
||||
} else {
|
||||
// we did not get an HTTP response from our last connection attempt.
|
||||
// DNS/TCP error, or a stall in the stream (and stall timer closed the connection)
|
||||
if (!self._usedFirstReconnect) {
|
||||
// first reconnection attempt on a valid connection should occur immediately
|
||||
self._connectInterval = 0;
|
||||
self._usedFirstReconnect = true;
|
||||
} else if (self._connectInterval < 16000) {
|
||||
// linearly increase delay by 250ms up to 16s
|
||||
self._connectInterval += 250;
|
||||
} else {
|
||||
// cap out reconnect interval at 16s
|
||||
self._connectInterval = 16000;
|
||||
}
|
||||
}
|
||||
|
||||
// schedule the reconnect
|
||||
self._scheduledReconnect = setTimeout(function () {
|
||||
self._startPersistentConnection();
|
||||
}, self._connectInterval);
|
||||
self.emit('reconnect', self.request, self.response, self._connectInterval);
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._setupParser = function () {
|
||||
var self = this
|
||||
self.parser = new Parser()
|
||||
|
||||
// handle twitter objects as they come in - emit the generic `message` event
|
||||
// along with the specific event corresponding to the message
|
||||
self.parser.on('element', function (msg) {
|
||||
self.emit('message', msg)
|
||||
|
||||
if (msg.delete) { self.emit('delete', msg) }
|
||||
else if (msg.disconnect) { self._handleDisconnect(msg) }
|
||||
else if (msg.limit) { self.emit('limit', msg) }
|
||||
else if (msg.scrub_geo) { self.emit('scrub_geo', msg) }
|
||||
else if (msg.warning) { self.emit('warning', msg) }
|
||||
else if (msg.status_withheld) { self.emit('status_withheld', msg) }
|
||||
else if (msg.user_withheld) { self.emit('user_withheld', msg) }
|
||||
else if (msg.friends || msg.friends_str) { self.emit('friends', msg) }
|
||||
else if (msg.direct_message) { self.emit('direct_message', msg) }
|
||||
else if (msg.event) {
|
||||
self.emit('user_event', msg)
|
||||
// reference: https://dev.twitter.com/docs/streaming-apis/messages#User_stream_messages
|
||||
var ev = msg.event
|
||||
|
||||
if (ev === 'blocked') { self.emit('blocked', msg) }
|
||||
else if (ev === 'unblocked') { self.emit('unblocked', msg) }
|
||||
else if (ev === 'favorite') { self.emit('favorite', msg) }
|
||||
else if (ev === 'unfavorite') { self.emit('unfavorite', msg) }
|
||||
else if (ev === 'follow') { self.emit('follow', msg) }
|
||||
else if (ev === 'unfollow') { self.emit('unfollow', msg) }
|
||||
else if (ev === 'user_update') { self.emit('user_update', msg) }
|
||||
else if (ev === 'list_created') { self.emit('list_created', msg) }
|
||||
else if (ev === 'list_destroyed') { self.emit('list_destroyed', msg) }
|
||||
else if (ev === 'list_updated') { self.emit('list_updated', msg) }
|
||||
else if (ev === 'list_member_added') { self.emit('list_member_added', msg) }
|
||||
else if (ev === 'list_member_removed') { self.emit('list_member_removed', msg) }
|
||||
else if (ev === 'list_user_subscribed') { self.emit('list_user_subscribed', msg) }
|
||||
else if (ev === 'list_user_unsubscribed') { self.emit('list_user_unsubscribed', msg) }
|
||||
else if (ev === 'quoted_tweet') { self.emit('quoted_tweet', msg) }
|
||||
else if (ev === 'favorited_retweet') { self.emit('favorited_retweet', msg) }
|
||||
else if (ev === 'retweeted_retweet') { self.emit('retweeted_retweet', msg) }
|
||||
else { self.emit('unknown_user_event', msg) }
|
||||
} else { self.emit('tweet', msg) }
|
||||
})
|
||||
|
||||
self.parser.on('error', function (err) {
|
||||
self.emit('parser-error', err)
|
||||
});
|
||||
self.parser.on('connection-limit-exceeded', function (err) {
|
||||
self.emit('error', err);
|
||||
})
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._handleDisconnect = function (twitterMsg) {
|
||||
this.emit('disconnect', twitterMsg);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever an http request is about to be made to update
|
||||
* our local timestamp (used for Oauth) to be Twitter's server time.
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._setOauthTimestamp = function () {
|
||||
var self = this;
|
||||
if (self.reqOpts.oauth) {
|
||||
var oauth_ts = Date.now() + self._twitter_time_minus_local_time_ms;
|
||||
self.reqOpts.oauth.timestamp = Math.floor(oauth_ts/1000).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever an http response is received from Twitter,
|
||||
* to set our local timestamp offset from Twitter's server time.
|
||||
* This is used to set the Oauth timestamp for our next http request
|
||||
* to Twitter (by calling _setOauthTimestamp).
|
||||
*
|
||||
* @param {http.IncomingResponse} resp http response received from Twitter.
|
||||
*/
|
||||
StreamingAPIConnection.prototype._updateOauthTimestampOffsetFromResponse = function (resp) {
|
||||
if (resp && resp.headers && resp.headers.date &&
|
||||
new Date(resp.headers.date).toString() !== 'Invalid Date'
|
||||
) {
|
||||
var twitterTimeMs = new Date(resp.headers.date).getTime()
|
||||
this._twitter_time_minus_local_time_ms = twitterTimeMs - Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StreamingAPIConnection
|
||||
485
node_modules/twit/lib/twitter.js
generated
vendored
Normal file
485
node_modules/twit/lib/twitter.js
generated
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
//
|
||||
// Twitter API Wrapper
|
||||
//
|
||||
var assert = require('assert');
|
||||
var Promise = require('bluebird');
|
||||
var request = require('request');
|
||||
var util = require('util');
|
||||
var endpoints = require('./endpoints');
|
||||
var FileUploader = require('./file_uploader');
|
||||
var helpers = require('./helpers');
|
||||
var StreamingAPIConnection = require('./streaming-api-connection');
|
||||
var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON;
|
||||
|
||||
// config values required for app-only auth
|
||||
var required_for_app_auth = [
|
||||
'consumer_key',
|
||||
'consumer_secret'
|
||||
];
|
||||
|
||||
// config values required for user auth (superset of app-only auth)
|
||||
var required_for_user_auth = required_for_app_auth.concat([
|
||||
'access_token',
|
||||
'access_token_secret'
|
||||
]);
|
||||
|
||||
var FORMDATA_PATHS = [
|
||||
'media/upload',
|
||||
'account/update_profile_image',
|
||||
'account/update_profile_background_image',
|
||||
];
|
||||
|
||||
var JSONPAYLOAD_PATHS = [
|
||||
'media/metadata/create'
|
||||
]
|
||||
|
||||
//
|
||||
// Twitter
|
||||
//
|
||||
var Twitter = function (config) {
|
||||
if (!(this instanceof Twitter)) {
|
||||
return new Twitter(config);
|
||||
}
|
||||
|
||||
var self = this
|
||||
var credentials = {
|
||||
consumer_key : config.consumer_key,
|
||||
consumer_secret : config.consumer_secret,
|
||||
// access_token and access_token_secret only required for user auth
|
||||
access_token : config.access_token,
|
||||
access_token_secret : config.access_token_secret,
|
||||
// flag indicating whether requests should be made with application-only auth
|
||||
app_only_auth : config.app_only_auth,
|
||||
}
|
||||
|
||||
this._validateConfigOrThrow(config);
|
||||
this.config = config;
|
||||
this._twitter_time_minus_local_time_ms = 0;
|
||||
}
|
||||
|
||||
Twitter.prototype.get = function (path, params, callback) {
|
||||
return this.request('GET', path, params, callback)
|
||||
}
|
||||
|
||||
Twitter.prototype.post = function (path, params, callback) {
|
||||
return this.request('POST', path, params, callback)
|
||||
}
|
||||
|
||||
Twitter.prototype.request = function (method, path, params, callback) {
|
||||
var self = this;
|
||||
assert(method == 'GET' || method == 'POST');
|
||||
// if no `params` is specified but a callback is, use default params
|
||||
if (typeof params === 'function') {
|
||||
callback = params
|
||||
params = {}
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var _returnErrorToUser = function (err) {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(err, null, null);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
self._buildReqOpts(method, path, params, false, function (err, reqOpts) {
|
||||
if (err) {
|
||||
_returnErrorToUser(err);
|
||||
return
|
||||
}
|
||||
|
||||
var twitOptions = (params && params.twit_options) || {};
|
||||
|
||||
process.nextTick(function () {
|
||||
// ensure all HTTP i/o occurs after the user has a chance to bind their event handlers
|
||||
self._doRestApiRequest(reqOpts, twitOptions, method, function (err, parsedBody, resp) {
|
||||
self._updateClockOffsetFromResponse(resp);
|
||||
|
||||
if (self.config.trusted_cert_fingerprints) {
|
||||
if (!resp.socket.authorized) {
|
||||
// The peer certificate was not signed by one of the authorized CA's.
|
||||
var authErrMsg = resp.socket.authorizationError.toString();
|
||||
var err = helpers.makeTwitError('The peer certificate was not signed; ' + authErrMsg);
|
||||
_returnErrorToUser(err);
|
||||
return;
|
||||
}
|
||||
var fingerprint = resp.socket.getPeerCertificate().fingerprint;
|
||||
var trustedFingerprints = self.config.trusted_cert_fingerprints;
|
||||
if (trustedFingerprints.indexOf(fingerprint) === -1) {
|
||||
var errMsg = util.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.',
|
||||
trustedFingerprints.join(','), fingerprint);
|
||||
var err = new Error(errMsg);
|
||||
_returnErrorToUser(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(err, parsedBody, resp);
|
||||
}
|
||||
|
||||
resolve({ data: parsedBody, resp: resp });
|
||||
return;
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to Twitter via the POST media/upload (chunked) API.
|
||||
* Use this as an easier alternative to doing the INIT/APPEND/FINALIZE commands yourself.
|
||||
* Returns the response from the FINALIZE command, or if an error occurs along the way,
|
||||
* the first argument to `cb` will be populated with a non-null Error.
|
||||
*
|
||||
*
|
||||
* `params` is an Object of the form:
|
||||
* {
|
||||
* file_path: String // Absolute path of file to be uploaded.
|
||||
* }
|
||||
*
|
||||
* @param {Object} params options object (described above).
|
||||
* @param {cb} cb callback of the form: function (err, bodyObj, resp)
|
||||
*/
|
||||
Twitter.prototype.postMediaChunked = function (params, cb) {
|
||||
var self = this;
|
||||
try {
|
||||
var fileUploader = new FileUploader(params, self);
|
||||
} catch(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
fileUploader.upload(cb);
|
||||
}
|
||||
|
||||
Twitter.prototype._updateClockOffsetFromResponse = function (resp) {
|
||||
var self = this;
|
||||
if (resp && resp.headers && resp.headers.date &&
|
||||
new Date(resp.headers.date).toString() !== 'Invalid Date'
|
||||
) {
|
||||
var twitterTimeMs = new Date(resp.headers.date).getTime()
|
||||
self._twitter_time_minus_local_time_ms = twitterTimeMs - Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns an options object ready to pass to `request()`
|
||||
* @param {String} method "GET" or "POST"
|
||||
* @param {String} path REST API resource uri (eg. "statuses/destroy/:id")
|
||||
* @param {Object} params user's params object
|
||||
* @param {Boolean} isStreaming Flag indicating if it's a request to the Streaming API (different endpoint)
|
||||
* @returns {Undefined}
|
||||
*
|
||||
* Calls `callback` with Error, Object where Object is an options object ready to pass to `request()`.
|
||||
*
|
||||
* Returns error raised (if any) by `helpers.moveParamsIntoPath()`
|
||||
*/
|
||||
Twitter.prototype._buildReqOpts = function (method, path, params, isStreaming, callback) {
|
||||
var self = this
|
||||
if (!params) {
|
||||
params = {}
|
||||
}
|
||||
// clone `params` object so we can modify it without modifying the user's reference
|
||||
var paramsClone = JSON.parse(JSON.stringify(params))
|
||||
// convert any arrays in `paramsClone` to comma-seperated strings
|
||||
var finalParams = this.normalizeParams(paramsClone)
|
||||
delete finalParams.twit_options
|
||||
|
||||
// the options object passed to `request` used to perform the HTTP request
|
||||
var reqOpts = {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'User-Agent': 'twit-client'
|
||||
},
|
||||
gzip: true,
|
||||
encoding: null,
|
||||
}
|
||||
|
||||
if (typeof self.config.timeout_ms !== 'undefined') {
|
||||
reqOpts.timeout = self.config.timeout_ms;
|
||||
}
|
||||
|
||||
try {
|
||||
// finalize the `path` value by building it using user-supplied params
|
||||
path = helpers.moveParamsIntoPath(finalParams, path)
|
||||
} catch (e) {
|
||||
callback(e, null, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (isStreaming) {
|
||||
// This is a Streaming API request.
|
||||
|
||||
var stream_endpoint_map = {
|
||||
user: endpoints.USER_STREAM,
|
||||
site: endpoints.SITE_STREAM
|
||||
}
|
||||
var endpoint = stream_endpoint_map[path] || endpoints.PUB_STREAM
|
||||
reqOpts.url = endpoint + path + '.json'
|
||||
} else {
|
||||
// This is a REST API request.
|
||||
|
||||
if (path.indexOf('media/') !== -1) {
|
||||
// For media/upload, use a different endpoint.
|
||||
reqOpts.url = endpoints.MEDIA_UPLOAD + path + '.json';
|
||||
} else {
|
||||
reqOpts.url = endpoints.REST_ROOT + path + '.json';
|
||||
}
|
||||
|
||||
if (FORMDATA_PATHS.indexOf(path) !== -1) {
|
||||
reqOpts.headers['Content-type'] = 'multipart/form-data';
|
||||
reqOpts.form = finalParams;
|
||||
// set finalParams to empty object so we don't append a query string
|
||||
// of the params
|
||||
finalParams = {};
|
||||
} else if (JSONPAYLOAD_PATHS.indexOf(path) !== -1) {
|
||||
reqOpts.headers['Content-type'] = 'application/json';
|
||||
reqOpts.json = true;
|
||||
reqOpts.body = finalParams;
|
||||
// as above, to avoid appending query string for body params
|
||||
finalParams = {};
|
||||
} else {
|
||||
reqOpts.headers['Content-type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(finalParams).length) {
|
||||
// not all of the user's parameters were used to build the request path
|
||||
// add them as a query string
|
||||
var qs = helpers.makeQueryString(finalParams)
|
||||
reqOpts.url += '?' + qs
|
||||
}
|
||||
|
||||
if (!self.config.app_only_auth) {
|
||||
// with user auth, we can just pass an oauth object to requests
|
||||
// to have the request signed
|
||||
var oauth_ts = Date.now() + self._twitter_time_minus_local_time_ms;
|
||||
|
||||
reqOpts.oauth = {
|
||||
consumer_key: self.config.consumer_key,
|
||||
consumer_secret: self.config.consumer_secret,
|
||||
token: self.config.access_token,
|
||||
token_secret: self.config.access_token_secret,
|
||||
timestamp: Math.floor(oauth_ts/1000).toString(),
|
||||
}
|
||||
|
||||
callback(null, reqOpts);
|
||||
return;
|
||||
} else {
|
||||
// we're using app-only auth, so we need to ensure we have a bearer token
|
||||
// Once we have a bearer token, add the Authorization header and return the fully qualified `reqOpts`.
|
||||
self._getBearerToken(function (err, bearerToken) {
|
||||
if (err) {
|
||||
callback(err, null)
|
||||
return
|
||||
}
|
||||
|
||||
reqOpts.headers['Authorization'] = 'Bearer ' + bearerToken;
|
||||
callback(null, reqOpts)
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request to Twitter REST API.
|
||||
* @param {Object} reqOpts options object passed to `request()`
|
||||
* @param {Object} twitOptions
|
||||
* @param {String} method "GET" or "POST"
|
||||
* @param {Function} callback user's callback
|
||||
* @return {Undefined}
|
||||
*/
|
||||
Twitter.prototype._doRestApiRequest = function (reqOpts, twitOptions, method, callback) {
|
||||
var request_method = request[method.toLowerCase()];
|
||||
var req = request_method(reqOpts);
|
||||
|
||||
var body = '';
|
||||
var response = null;
|
||||
|
||||
var onRequestComplete = function () {
|
||||
if (body !== '') {
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (jsonDecodeError) {
|
||||
// there was no transport-level error, but a JSON object could not be decoded from the request body
|
||||
// surface this to the caller
|
||||
var err = helpers.makeTwitError('JSON decode error: Twitter HTTP response body was not valid JSON')
|
||||
err.statusCode = response ? response.statusCode: null;
|
||||
err.allErrors.concat({error: jsonDecodeError.toString()})
|
||||
callback(err, body, response);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object' && (body.error || body.errors)) {
|
||||
// we got a Twitter API-level error response
|
||||
// place the errors in the HTTP response body into the Error object and pass control to caller
|
||||
var err = helpers.makeTwitError('Twitter API Error')
|
||||
err.statusCode = response ? response.statusCode: null;
|
||||
helpers.attachBodyInfoToError(err, body);
|
||||
callback(err, body, response);
|
||||
return
|
||||
}
|
||||
|
||||
// success case - no errors in HTTP response body
|
||||
callback(err, body, response)
|
||||
}
|
||||
|
||||
req.on('response', function (res) {
|
||||
response = res
|
||||
// read data from `request` object which contains the decompressed HTTP response body,
|
||||
// `response` is the unmodified http.IncomingMessage object which may contain compressed data
|
||||
req.on('data', function (chunk) {
|
||||
body += chunk.toString('utf8')
|
||||
})
|
||||
// we're done reading the response
|
||||
req.on('end', function () {
|
||||
onRequestComplete()
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', function (err) {
|
||||
// transport-level error occurred - likely a socket error
|
||||
if (twitOptions.retry &&
|
||||
STATUS_CODES_TO_ABORT_ON.indexOf(err.statusCode) !== -1
|
||||
) {
|
||||
// retry the request since retries were specified and we got a status code we should retry on
|
||||
self.request(method, path, params, callback);
|
||||
return;
|
||||
} else {
|
||||
// pass the transport-level error to the caller
|
||||
err.statusCode = null
|
||||
err.code = null
|
||||
err.allErrors = [];
|
||||
helpers.attachBodyInfoToError(err, body)
|
||||
callback(err, body, response);
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/starts a connection object that stays connected to Twitter's servers
|
||||
* using Twitter's rules.
|
||||
*
|
||||
* @param {String} path Resource path to connect to (eg. "statuses/sample")
|
||||
* @param {Object} params user's params object
|
||||
* @return {StreamingAPIConnection} [description]
|
||||
*/
|
||||
Twitter.prototype.stream = function (path, params) {
|
||||
var self = this;
|
||||
var twitOptions = (params && params.twit_options) || {};
|
||||
|
||||
var streamingConnection = new StreamingAPIConnection()
|
||||
self._buildReqOpts('POST', path, params, true, function (err, reqOpts) {
|
||||
if (err) {
|
||||
// we can get an error if we fail to obtain a bearer token or construct reqOpts
|
||||
// surface this on the streamingConnection instance (where a user may register their error handler)
|
||||
streamingConnection.emit('error', err)
|
||||
return
|
||||
}
|
||||
// set the properties required to start the connection
|
||||
streamingConnection.reqOpts = reqOpts
|
||||
streamingConnection.twitOptions = twitOptions
|
||||
|
||||
process.nextTick(function () {
|
||||
streamingConnection.start()
|
||||
})
|
||||
})
|
||||
|
||||
return streamingConnection
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets bearer token from cached reference on `self`, or fetches a new one and sets it on `self`.
|
||||
*
|
||||
* @param {Function} callback Function to invoke with (Error, bearerToken)
|
||||
* @return {Undefined}
|
||||
*/
|
||||
Twitter.prototype._getBearerToken = function (callback) {
|
||||
var self = this;
|
||||
if (self._bearerToken) {
|
||||
return callback(null, self._bearerToken)
|
||||
}
|
||||
|
||||
helpers.getBearerToken(self.config.consumer_key, self.config.consumer_secret,
|
||||
function (err, bearerToken) {
|
||||
if (err) {
|
||||
// return the fully-qualified Twit Error object to caller
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
self._bearerToken = bearerToken;
|
||||
callback(null, self._bearerToken);
|
||||
return;
|
||||
})
|
||||
}
|
||||
|
||||
Twitter.prototype.normalizeParams = function (params) {
|
||||
var normalized = params
|
||||
if (params && typeof params === 'object') {
|
||||
Object.keys(params).forEach(function (key) {
|
||||
var value = params[key]
|
||||
// replace any arrays in `params` with comma-separated string
|
||||
if (Array.isArray(value))
|
||||
normalized[key] = value.join(',')
|
||||
})
|
||||
} else if (!params) {
|
||||
normalized = {}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
Twitter.prototype.setAuth = function (auth) {
|
||||
var self = this
|
||||
var configKeys = [
|
||||
'consumer_key',
|
||||
'consumer_secret',
|
||||
'access_token',
|
||||
'access_token_secret'
|
||||
];
|
||||
|
||||
// update config
|
||||
configKeys.forEach(function (k) {
|
||||
if (auth[k]) {
|
||||
self.config[k] = auth[k]
|
||||
}
|
||||
})
|
||||
this._validateConfigOrThrow(self.config);
|
||||
}
|
||||
|
||||
Twitter.prototype.getAuth = function () {
|
||||
return this.config
|
||||
}
|
||||
|
||||
//
|
||||
// Check that the required auth credentials are present in `config`.
|
||||
// @param {Object} config Object containing credentials for REST API auth
|
||||
//
|
||||
Twitter.prototype._validateConfigOrThrow = function (config) {
|
||||
//check config for proper format
|
||||
if (typeof config !== 'object') {
|
||||
throw new TypeError('config must be object, got ' + typeof config)
|
||||
}
|
||||
|
||||
if (typeof config.timeout_ms !== 'undefined' && isNaN(Number(config.timeout_ms))) {
|
||||
throw new TypeError('Twit config `timeout_ms` must be a Number. Got: ' + config.timeout_ms + '.');
|
||||
}
|
||||
|
||||
if (config.app_only_auth) {
|
||||
var auth_type = 'app-only auth'
|
||||
var required_keys = required_for_app_auth
|
||||
} else {
|
||||
var auth_type = 'user auth'
|
||||
var required_keys = required_for_user_auth
|
||||
}
|
||||
|
||||
required_keys.forEach(function (req_key) {
|
||||
if (!config[req_key]) {
|
||||
var err_msg = util.format('Twit config must include `%s` when using %s.', req_key, auth_type)
|
||||
throw new Error(err_msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = Twitter
|
||||
Reference in New Issue
Block a user