Skip to content

fix default so that if the version is 2, it will not append .json #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 88 additions & 85 deletions twitter.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
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");
},
});

return client;
};

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,
};

Expand All @@ -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,
Expand All @@ -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;
}

Expand All @@ -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()),
};
}
}
Expand Down Expand Up @@ -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);

Expand All @@ -152,42 +152,44 @@ 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;
}

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;
}
Expand All @@ -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 = {
Expand All @@ -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);
}

/**
Expand All @@ -253,25 +256,24 @@ 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);
if (JSON_ENDPOINTS.includes(resource)) {
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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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;
}
Expand Down