Skip to content

Commit d3cb8f2

Browse files
committed
🚀 v0.8: submit POST params in body. Encode special chars. Streaming test. Fix #36.
1 parent 2ecfa30 commit d3cb8f2

File tree

5 files changed

+1482
-643
lines changed

5 files changed

+1482
-643
lines changed

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "twitter-lite",
3-
"version": "0.7.0",
3+
"version": "0.8.0",
44
"description": "A tiny, full-featured client / server library for the Twitter API",
55
"source": [
66
"twitter.js",

‎stream.test.js‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,129 @@
1+
require("dotenv").config();
12
const Stream = require("./stream");
3+
const Twitter = require("./twitter");
4+
5+
const {
6+
TWITTER_CONSUMER_KEY,
7+
TWITTER_CONSUMER_SECRET,
8+
ACCESS_TOKEN,
9+
ACCESS_TOKEN_SECRET
10+
} = process.env;
11+
12+
function newClient(subdomain = "api") {
13+
return new Twitter({
14+
subdomain,
15+
consumer_key: TWITTER_CONSUMER_KEY,
16+
consumer_secret: TWITTER_CONSUMER_SECRET,
17+
access_token_key: ACCESS_TOKEN,
18+
access_token_secret: ACCESS_TOKEN_SECRET
19+
});
20+
}
221

322
it("should default export to be a function", () => {
423
expect(new Stream()).toBeInstanceOf(Stream);
524
});
25+
26+
describe("functionality", () => {
27+
let client;
28+
beforeAll(() => (client = newClient()));
29+
30+
it("should filter realtime tweets from up to 5000 users", done => {
31+
// https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter
32+
const stream = client.stream("statuses/filter", {
33+
follow: [
34+
// First pass a ton of accounts that don't tweet often (@dandv), to stress-test the POST body
35+
...Array(4900).fill("15008676"),
36+
// Then add prolific users from https://socialblade.com/twitter/top/100/tweets that are
37+
// still active. Get with $('div.table-cell a').map(function () { return this.href })
38+
// then use users/lookup to convert to IDs.
39+
"63299591",
40+
"115639376",
41+
"4823945834",
42+
"2669983818",
43+
"6529402",
44+
"362413805",
45+
"450395397",
46+
"15007299",
47+
"132355708",
48+
"561669474",
49+
"2213312341",
50+
"2050001283",
51+
"89142182",
52+
"2316574981",
53+
"133684052",
54+
"255409050",
55+
"15518000",
56+
"124172948",
57+
"225647847",
58+
"3012764258",
59+
"382430644",
60+
"42832810",
61+
"2233720891",
62+
"290395312",
63+
"50706690",
64+
"1388673048",
65+
"414306138",
66+
"155409802",
67+
"21976463",
68+
"1179710990",
69+
"130426181",
70+
"171299971",
71+
"32453798",
72+
"22279680",
73+
"22274998",
74+
"59804598",
75+
"3048544857",
76+
"17872077",
77+
"85741735",
78+
"3032932864",
79+
"120421476",
80+
"473656787",
81+
"876302191",
82+
"717628618906570752",
83+
"15518784",
84+
"152641509",
85+
"5950272",
86+
"416383737",
87+
"2569759392",
88+
"165796189",
89+
"1680484418",
90+
"108192135",
91+
"3007312628",
92+
"32771325",
93+
"764410142679035904",
94+
"19272300",
95+
"829411574",
96+
"68956490",
97+
"2836271637",
98+
"392599269",
99+
"1145130336",
100+
"52236744",
101+
"243133079",
102+
"104120518",
103+
"51684249",
104+
"18057450",
105+
"1027850761",
106+
"1868107663",
107+
"213165296",
108+
"15503908",
109+
"1346933186",
110+
"2857426909",
111+
"2814731582",
112+
"453780255",
113+
"3027662932",
114+
"23719043",
115+
"486288760",
116+
"121190725",
117+
"2942062137",
118+
"19286574",
119+
"21033096",
120+
"271986064"
121+
]
122+
});
123+
124+
stream.on("data", tweet => {
125+
// Within seconds, one of those prolific accounts will tweet something
126+
done();
127+
});
128+
});
129+
});

‎twitter.js‎

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,31 @@ const defaults = {
3131
bearer_token: null
3232
};
3333

34+
// Twitter expects POST body parameters to be URL-encoded: https://developer.twitter.com/en/docs/basics/authentication/guides/creating-a-signature
35+
// However, some endpoints expect a JSON payload - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
36+
// It appears that JSON payloads don't need to be included in the signature,
37+
// because sending DMs works without signing the POST body
38+
const JSON_ENDPOINTS = [
39+
"direct_messages/events/new",
40+
"direct_messages/welcome_messages/new",
41+
"direct_messages/welcome_messages/rules/new"
42+
];
43+
3444
const baseHeaders = {
3545
"Content-Type": "application/json",
3646
Accept: "application/json"
3747
};
3848

49+
function percentEncode(string) {
50+
// From OAuth.prototype.percentEncode
51+
return string
52+
.replace(/!/g, "%21")
53+
.replace(/\*/g, "%2A")
54+
.replace(/'/g, "%27")
55+
.replace(/\(/g, "%28")
56+
.replace(/\)/g, "%29");
57+
}
58+
3959
class Twitter {
4060
constructor(options) {
4161
const config = Object.assign({}, defaults, options);
@@ -64,7 +84,7 @@ class Twitter {
6484
static _handleResponse(response) {
6585
const headers = response.headers.raw(); // https://github.com/bitinn/node-fetch/issues/495
6686
return response.json().then(res => {
67-
res._headers = headers;
87+
res._headers = headers; // TODO: this creates an array-like object when it adds _headers to an array response
6888
return res;
6989
});
7090
}
@@ -151,7 +171,9 @@ class Twitter {
151171
url: `${this.url}/${resource}.json`,
152172
method
153173
};
154-
if (parameters) requestData.url += "?" + querystring.stringify(parameters);
174+
if (parameters)
175+
if (method === "POST") requestData.data = parameters;
176+
else requestData.url += "?" + querystring.stringify(parameters);
155177

156178
let headers = {};
157179
if (this.authType === "User") {
@@ -178,8 +200,8 @@ class Twitter {
178200

179201
return Fetch(requestData.url, { headers })
180202
.then(Twitter._handleResponse)
181-
.then(
182-
results => ("errors" in results ? Promise.reject(results) : results)
203+
.then(results =>
204+
"errors" in results ? Promise.reject(results) : results
183205
);
184206
}
185207

@@ -190,14 +212,22 @@ class Twitter {
190212
parameters
191213
);
192214

215+
const postHeaders = Object.assign({}, baseHeaders, headers);
216+
if (JSON_ENDPOINTS.includes(resource)) {
217+
body = JSON.stringify(body);
218+
} else {
219+
body = querystring.stringify(parameters);
220+
postHeaders["Content-Type"] = "application/x-www-form-urlencoded";
221+
}
222+
193223
return Fetch(requestData.url, {
194224
method: "POST",
195-
headers: Object.assign({}, baseHeaders, headers),
196-
body: JSON.stringify(body)
225+
headers: postHeaders,
226+
body: percentEncode(body)
197227
})
198228
.then(Twitter._handleResponse)
199-
.then(
200-
results => ("errors" in results ? Promise.reject(results) : results)
229+
.then(results =>
230+
"errors" in results ? Promise.reject(results) : results
201231
);
202232
}
203233

@@ -207,17 +237,26 @@ class Twitter {
207237

208238
const stream = new Stream();
209239

240+
// POST the request, in order to accommodate long parameter lists, e.g.
241+
// up to 5000 ids for statuses/filter - https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter
210242
const requestData = {
211243
url: `${getUrl("stream")}/${resource}.json`,
212-
method: "GET"
244+
method: "POST"
213245
};
214-
if (parameters) requestData.url += "?" + querystring.stringify(parameters);
246+
if (parameters) requestData.data = parameters;
215247

216248
const headers = this.client.toHeader(
217249
this.client.authorize(requestData, this.token)
218250
);
219251

220-
const request = Fetch(requestData.url, { headers });
252+
const request = Fetch(requestData.url, {
253+
method: "POST",
254+
headers: {
255+
...headers,
256+
"Content-Type": "application/x-www-form-urlencoded"
257+
},
258+
body: percentEncode(querystring.stringify(parameters))
259+
});
221260

222261
request
223262
.then(response => {

0 commit comments

Comments
 (0)