diff --git a/twitter.js b/twitter.js index e6df503..6c392e6 100644 --- a/twitter.js +++ b/twitter.js @@ -1,21 +1,18 @@ -const crypto = require('crypto'); -const OAuth = require('oauth-1.0a'); -const Fetch = require('cross-fetch'); -const querystring = require('querystring'); -const Stream = require('./stream'); +const crypto = require("crypto"); +const OAuth = require("oauth-1.0a"); +const Fetch = require("cross-fetch"); +const querystring = require("querystring"); +const Stream = require("./stream"); -const getUrl = (subdomain, endpoint = '1.1') => +const getUrl = (subdomain, endpoint = "21.1") => `https://${subdomain}.twitter.com/${endpoint}`; const createOauthClient = ({ key, secret }) => { const client = OAuth({ consumer: { key, secret }, - signature_method: 'HMAC-SHA1', + signature_method: "HMAC-SHA1", hash_function(baseString, key) { - return crypto - .createHmac('sha1', key) - .update(baseString) - .digest('base64'); + return crypto.createHmac("sha1", key).update(baseString).digest("base64"); }, }); @@ -23,13 +20,13 @@ const createOauthClient = ({ key, secret }) => { }; const defaults = { - subdomain: 'api', + subdomain: "api", consumer_key: null, consumer_secret: null, access_token_key: null, access_token_secret: null, bearer_token: null, - version: '1.1', + version: "1.1", extension: true, }; @@ -38,32 +35,32 @@ const defaults = { // It appears that JSON payloads don't need to be included in the signature, // because sending DMs works without signing the POST body const JSON_ENDPOINTS = [ - 'direct_messages/events/new', - 'direct_messages/welcome_messages/new', - 'direct_messages/welcome_messages/rules/new', - 'media/metadata/create', - 'collections/entries/curate', + "direct_messages/events/new", + "direct_messages/welcome_messages/new", + "direct_messages/welcome_messages/rules/new", + "media/metadata/create", + "collections/entries/curate", ]; const baseHeaders = { - 'Content-Type': 'application/json', - Accept: 'application/json', + "Content-Type": "application/json", + Accept: "application/json", }; function percentEncode(string) { // From OAuth.prototype.percentEncode return string - .replace(/!/g, '%21') - .replace(/\*/g, '%2A') - .replace(/'/g, '%27') - .replace(/\(/g, '%28') - .replace(/\)/g, '%29'); + .replace(/!/g, "%21") + .replace(/\*/g, "%2A") + .replace(/'/g, "%27") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); } class Twitter { constructor(options) { const config = Object.assign({}, defaults, options); - this.authType = config.bearer_token ? 'App' : 'User'; + this.authType = config.bearer_token ? "App" : "User"; this.client = createOauthClient({ key: config.consumer_key, secret: config.consumer_secret, @@ -75,7 +72,7 @@ class Twitter { }; this.url = getUrl(config.subdomain, config.version); - this.oauth = getUrl(config.subdomain, 'oauth'); + this.oauth = getUrl(config.subdomain, "oauth"); this.config = config; } @@ -89,19 +86,22 @@ class Twitter { const headers = response.headers; // TODO: see #44 if (response.ok) { // Return empty response on 204 "No content", or Content-Length=0 - if (response.status === 204 || response.headers.get('content-length') === '0') + if ( + response.status === 204 || + response.headers.get("content-length") === "0" + ) return { _headers: headers, }; // Otherwise, parse JSON response - return response.json().then(res => { + return response.json().then((res) => { res._headers = headers; // TODO: this creates an array-like object when it adds _headers to an array response return res; }); } else { throw { _headers: headers, - ...await response.json(), + ...(await response.json()), }; } } @@ -133,16 +133,16 @@ class Twitter { async getBearerToken() { const headers = { Authorization: - 'Basic ' + + "Basic " + Buffer.from( - this.config.consumer_key + ':' + this.config.consumer_secret, - ).toString('base64'), - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + this.config.consumer_key + ":" + this.config.consumer_secret + ).toString("base64"), + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }; - const results = await Fetch('https://api.twitter.com/oauth2/token', { - method: 'POST', - body: 'grant_type=client_credentials', + const results = await Fetch("https://api.twitter.com/oauth2/token", { + method: "POST", + body: "grant_type=client_credentials", headers, }).then(Twitter._handleResponse); @@ -152,22 +152,21 @@ class Twitter { async getRequestToken(twitterCallbackUrl) { const requestData = { url: `${this.oauth}/request_token`, - method: 'POST', + method: "POST", }; let parameters = {}; if (twitterCallbackUrl) parameters = { oauth_callback: twitterCallbackUrl }; - if (parameters) requestData.url += '?' + querystring.stringify(parameters); + if (parameters) requestData.url += "?" + querystring.stringify(parameters); const headers = this.client.toHeader( - this.client.authorize(requestData, {}), + this.client.authorize(requestData, {}) ); const results = await Fetch(requestData.url, { - method: 'POST', + method: "POST", headers: Object.assign({}, baseHeaders, headers), - }) - .then(Twitter._handleResponseTextOrJson); + }).then(Twitter._handleResponseTextOrJson); return results; } @@ -175,19 +174,22 @@ class Twitter { async getAccessToken(options) { const requestData = { url: `${this.oauth}/access_token`, - method: 'POST', + method: "POST", }; - let parameters = { oauth_verifier: options.oauth_verifier, oauth_token: options.oauth_token }; - if (parameters.oauth_verifier && parameters.oauth_token) requestData.url += '?' + querystring.stringify(parameters); + let parameters = { + oauth_verifier: options.oauth_verifier, + oauth_token: options.oauth_token, + }; + if (parameters.oauth_verifier && parameters.oauth_token) + requestData.url += "?" + querystring.stringify(parameters); - const headers = this.client.toHeader( this.client.authorize(requestData) ); + const headers = this.client.toHeader(this.client.authorize(requestData)); const results = await Fetch(requestData.url, { - method: 'POST', + method: "POST", headers: Object.assign({}, baseHeaders, headers), - }) - .then(Twitter._handleResponseTextOrJson); + }).then(Twitter._handleResponseTextOrJson); return results; } @@ -202,17 +204,19 @@ class Twitter { */ _makeRequest(method, resource, parameters) { const requestData = { - url: `${this.url}/${resource}${this.config.extension ? '.json' : ''}`, + url: `${this.url}/${resource}${ + !this.config.extension || this.config.version == "2" ? "" : ".json" + }`, method, }; if (parameters) - if (method === 'POST') requestData.data = parameters; - else requestData.url += '?' + querystring.stringify(parameters); + if (method === "POST") requestData.data = parameters; + else requestData.url += "?" + querystring.stringify(parameters); let headers = {}; - if (this.authType === 'User') { + if (this.authType === "User") { headers = this.client.toHeader( - this.client.authorize(requestData, this.token), + this.client.authorize(requestData, this.token) ); } else { headers = { @@ -234,13 +238,12 @@ class Twitter { */ get(resource, parameters) { const { requestData, headers } = this._makeRequest( - 'GET', + "GET", resource, - parameters, + parameters ); - return Fetch(requestData.url, { headers }) - .then(Twitter._handleResponse); + return Fetch(requestData.url, { headers }).then(Twitter._handleResponse); } /** @@ -253,9 +256,9 @@ class Twitter { */ post(resource, body) { const { requestData, headers } = this._makeRequest( - 'POST', + "POST", resource, - JSON_ENDPOINTS.includes(resource) ? null : body, // don't sign JSON bodies; only parameters + JSON_ENDPOINTS.includes(resource) ? null : body // don't sign JSON bodies; only parameters ); const postHeaders = Object.assign({}, baseHeaders, headers); @@ -263,15 +266,14 @@ class Twitter { body = JSON.stringify(body); } else { body = percentEncode(querystring.stringify(body)); - postHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + postHeaders["Content-Type"] = "application/json"; } return Fetch(requestData.url, { - method: 'POST', + method: "POST", headers: postHeaders, body, - }) - .then(Twitter._handleResponse); + }).then(Twitter._handleResponse); } /** @@ -283,20 +285,19 @@ class Twitter { */ put(resource, parameters, body) { const { requestData, headers } = this._makeRequest( - 'PUT', + "PUT", resource, - parameters, + parameters ); const putHeaders = Object.assign({}, baseHeaders, headers); body = JSON.stringify(body); return Fetch(requestData.url, { - method: 'PUT', + method: "PUT", headers: putHeaders, body, - }) - .then(Twitter._handleResponse); + }).then(Twitter._handleResponse); } /** @@ -306,49 +307,51 @@ class Twitter { * @returns {Stream} */ stream(resource, parameters) { - if (this.authType !== 'User') - throw new Error('Streams require user context authentication'); + if (this.authType !== "User") + throw new Error("Streams require user context authentication"); const stream = new Stream(); // POST the request, in order to accommodate long parameter lists, e.g. // up to 5000 ids for statuses/filter - https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter const requestData = { - url: `${getUrl('stream')}/${resource}${this.config.extension ? '.json' : ''}`, - method: 'POST', + url: `${getUrl("stream")}/${resource}${ + this.config.extension ? ".json" : "" + }`, + method: "POST", }; if (parameters) requestData.data = parameters; const headers = this.client.toHeader( - this.client.authorize(requestData, this.token), + this.client.authorize(requestData, this.token) ); const request = Fetch(requestData.url, { - method: 'POST', + method: "POST", headers: { ...headers, - 'Content-Type': 'application/x-www-form-urlencoded', + "Content-Type": "application/x-www-form-urlencoded", }, body: percentEncode(querystring.stringify(parameters)), }); request - .then(response => { + .then((response) => { stream.destroy = this.stream.destroy = () => response.body.destroy(); if (response.ok) { - stream.emit('start', response); + stream.emit("start", response); } else { - response._headers = response.headers; // TODO: see #44 - could omit the line - stream.emit('error', response); + response._headers = response.headers; // TODO: see #44 - could omit the line + stream.emit("error", response); } response.body - .on('data', chunk => stream.parse(chunk)) - .on('error', error => stream.emit('error', error)) // no point in adding the original response headers - .on('end', () => stream.emit('end', response)); + .on("data", (chunk) => stream.parse(chunk)) + .on("error", (error) => stream.emit("error", error)) // no point in adding the original response headers + .on("end", () => stream.emit("end", response)); }) - .catch(error => stream.emit('error', error)); + .catch((error) => stream.emit("error", error)); return stream; }