Skip to content

Commit 4d12951

Browse files
committed
Added Twitter Labs filter stream v1 support
In detail: - added API calls for adding, deleting and getting filter stream rules - added stream API call - added appropriate types - attempted to stick with current code and exporting structure
1 parent 084a68d commit 4d12951

File tree

4 files changed

+426
-17
lines changed

4 files changed

+426
-17
lines changed

index.d.ts

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ export default class Twitter {
111111
* @returns {Stream}
112112
*/
113113
public stream(resource: string, parameters: object): Stream;
114+
115+
/**
116+
* Creates an instance of the original {@instance Twitter} with labs API capabilities
117+
* @return {TwitterLabs} - a twitter labs instance
118+
*/
119+
public withLabs(): TwitterLabs;
120+
121+
/**
122+
* Add rule for the filter stream API
123+
*
124+
* @param {LabsFilterStreamRule[]} rules a list of rules for the filter stream API
125+
* @param {boolean} [dryRun] optional parameter to mark the request as a dry run
126+
* @returns {Promise<object>} Promise response from Twitter API
127+
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/post-tweets-stream-filter-rules Twitter API}
128+
*/
129+
public static labsFilterStreamRule(value: string, tag?: string): FilterStreamRule;
114130
}
115131

116132
/* In reality snowflakes are BigInts. Once BigInt is supported by browsers and Node per default, we could adjust this type.
@@ -157,10 +173,10 @@ interface BearerResponse {
157173

158174
type TokenResponse =
159175
| {
160-
oauth_token: OauthToken;
161-
oauth_token_secret: OauthTokenSecret;
162-
oauth_callback_confirmed: 'true';
163-
}
176+
oauth_token: OauthToken;
177+
oauth_token_secret: OauthTokenSecret;
178+
oauth_callback_confirmed: 'true';
179+
}
164180
| { oauth_callback_confirmed: 'false' };
165181

166182
interface AccessTokenResponse {
@@ -176,3 +192,82 @@ declare class Stream extends EventEmitter {
176192
parse(buffer: Buffer): void;
177193
destroy(): void;
178194
}
195+
196+
export class TwitterLabs extends Twitter {
197+
/**
198+
* Construct the data and headers for an authenticated HTTP request to the Twitter Labs API
199+
* @param {'GET | 'POST' | 'PUT'} method
200+
* @param {'1' | '2'} version
201+
* @param {string} resource - the API endpoint
202+
* @param {object} queryParams - query params object
203+
*/
204+
private _makeLabsRequest(method: 'GET' | 'POST' | 'PUT', version: '1' | '2',
205+
resource: string, queryParams: object): {
206+
requestData: { url: string; method: string };
207+
headers: { Authorization: string } | OAuth.Header;
208+
};
209+
210+
/**
211+
* Add rule for the filter stream API
212+
*
213+
* @param {FilterStreamRule[]} rules a list of rules for the filter stream API
214+
* @param {boolean} [dryRun] optional parameter to mark the request as a dry run
215+
* @returns {Promise<object>} Promise response from Twitter API
216+
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/post-tweets-stream-filter-rules Twitter API}
217+
*/
218+
public addRules(rules: FilterStreamRule[], dryRun?: boolean): Promise<object>
219+
220+
/**
221+
* Get registered rules
222+
*
223+
* @returns {Promise<object>} Promise response from Twitter API
224+
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter-rules Twitter API}
225+
*/
226+
public getRules(...ids: string[]): Promise<object>
227+
228+
/**
229+
* Delete registered rules
230+
*
231+
* @param {string[]} Rule IDs that has been registered
232+
* @param {boolean} [dryRun] optional parameter to mark request as a dry run
233+
* @returns {Promise<object>} Promise response from Twitter API
234+
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter-rules Twitter API}
235+
*/
236+
public deleteRules(ids: string[], dryRun?: boolean): Promise<object>
237+
238+
239+
/**
240+
* Start filter stream using saved rules
241+
*
242+
* @param {{expansions: Expansions[], format: Format, 'place.format': Format,
243+
* 'tweet.format': Format, 'user.format': Format}} [queryParams]
244+
* @returns {Stream} stream object for the filter stream
245+
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter Twitter API}
246+
*/
247+
filterStream(queryParams?: FilterStreamParams): Promise<object>
248+
}
249+
250+
/**
251+
* Rule structure when adding twitter labs filter stream rules
252+
*/
253+
type FilterStreamRule = { value: string, meta?: string };
254+
255+
/**
256+
* Twitter labs response format
257+
* @see {@link https://developer.twitter.com/en/docs/labs/overview/whats-new/formats About format}
258+
*/
259+
type LabsFormat = 'compact' | 'detailed' | 'default';
260+
261+
/**
262+
* Twitter labs expansions
263+
* @see {@link https://developer.twitter.com/en/docs/labs/overview/whats-new/expansions About expansions}
264+
*/
265+
type LabsExpansion = 'attachment.poll_ids' | 'attachments.media_keys' | 'author_id' | 'entities.mentions.username' | 'geo.place_id'
266+
| 'in_reply_to_user_id' | 'referenced_tweets.id' | 'referenced_tweets.id.author_id';
267+
type FilterStreamParams = {
268+
expansions?: LabsExpansion[],
269+
format?: LabsFormat,
270+
'place.format'?: LabsFormat,
271+
'tweet.format'?: LabsFormat,
272+
'user.format'?: LabsFormat
273+
};

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"scripts": {
4848
"lint": "eslint --fix ./",
4949
"prepare": "microbundle {stream,twitter}.js && bundlesize",
50-
"test": "eslint --fix . && jest --detectOpenHandles",
50+
"test": "eslint --fix . && jest --testPathIgnorePatterns=labs --detectOpenHandles",
51+
"test-labs": "eslint --fix . && jest --testPathPattern=labs --detectOpenHandles",
5152
"release": "npm run -s prepare && npm test && git tag $npm_package_version && git push && git push --tags && npm publish"
5253
},
5354
"husky": {
@@ -68,4 +69,4 @@
6869
"maxSize": "3 kB"
6970
}
7071
]
71-
}
72+
}

test/twitter.labs.test.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
require('dotenv').config();
2+
const Twitter = require('../twitter');
3+
4+
const {
5+
TWITTER_CONSUMER_KEY,
6+
TWITTER_CONSUMER_SECRET,
7+
ACCESS_TOKEN,
8+
ACCESS_TOKEN_SECRET,
9+
} = process.env;
10+
11+
function newClient() {
12+
return new Twitter({
13+
consumer_key: TWITTER_CONSUMER_KEY,
14+
consumer_secret: TWITTER_CONSUMER_SECRET,
15+
access_token_key: ACCESS_TOKEN,
16+
access_token_secret: ACCESS_TOKEN_SECRET,
17+
});
18+
}
19+
20+
21+
describe('LABS - creating with labs', () => {
22+
let client;
23+
let clientWithLabs;
24+
beforeAll(() => {
25+
client = newClient();
26+
clientWithLabs = client.withLabs();
27+
});
28+
29+
it('should create object with all twitter functions', () => {
30+
for (const funcName of Object.getOwnPropertyNames(Twitter.prototype)) {
31+
expect(clientWithLabs[funcName]).toBeDefined();
32+
expect(clientWithLabs[funcName]).toBeInstanceOf(Function);
33+
}
34+
});
35+
36+
it('should create object with all twitter properties', () => {
37+
for (const propertyName of Object.getOwnPropertyNames(client)) {
38+
expect(clientWithLabs[propertyName]).toBeDefined();
39+
expect(clientWithLabs[propertyName]);
40+
}
41+
});
42+
});
43+
44+
describe('LABS - filter stream labs', () => {
45+
let clientWithLabs;
46+
let addedRules;
47+
let addedRulesId;
48+
49+
// create labs instance and add initial rules
50+
beforeAll(async () => {
51+
const bearerToken = await newClient().getBearerToken();
52+
clientWithLabs = new Twitter({ bearer_token: bearerToken.access_token }).withLabs();
53+
const rulesToAdd = [
54+
Twitter.labsFilterStreamRule('twitter'),
55+
Twitter.labsFilterStreamRule('testing'),
56+
Twitter.labsFilterStreamRule('hello'),
57+
];
58+
const response = await clientWithLabs.addRules(rulesToAdd);
59+
addedRules = response.data;
60+
addedRulesId = response.data.map(d => d.id);
61+
});
62+
63+
// delete initialized rules
64+
afterAll(async () => {
65+
await clientWithLabs.deleteRules(addedRulesId);
66+
});
67+
68+
it('should create new rules when adding non-existent rules', async () => {
69+
const rulesToAdd = [Twitter.labsFilterStreamRule('random1'), Twitter.labsFilterStreamRule('random2')];
70+
const addRulesResponse = await clientWithLabs.addRules(rulesToAdd, true);
71+
72+
expect(addRulesResponse).toMatchObject({
73+
data: [
74+
{ value: 'random1', id: expect.any(String) },
75+
{ value: 'random2', id: expect.any(String) },
76+
],
77+
meta: {
78+
summary: {
79+
created: 2,
80+
},
81+
},
82+
});
83+
});
84+
85+
it('should not create new rules when adding existing rules', async () => {
86+
const rulesToAdd = [Twitter.labsFilterStreamRule('twitter'), Twitter.labsFilterStreamRule('testing')];
87+
const addRulesResponse = await clientWithLabs.addRules(rulesToAdd, true);
88+
89+
expect(addRulesResponse).toMatchObject({
90+
meta: {
91+
summary: {
92+
created: 0,
93+
},
94+
},
95+
});
96+
});
97+
98+
it('should delete rules that exist', async () => {
99+
const deleteRulesResponse = await clientWithLabs.deleteRules(addedRulesId, true);
100+
101+
expect(deleteRulesResponse).toMatchObject({
102+
meta: {
103+
summary: {
104+
deleted: addedRulesId.length,
105+
},
106+
},
107+
});
108+
});
109+
110+
it('should be an error when deleting rules that does not exist', async () => {
111+
const deleteRulesResponse = await clientWithLabs.deleteRules(['239197139192', '28319317192'], true);
112+
113+
expect(deleteRulesResponse).toMatchObject({
114+
meta: {
115+
summary: {
116+
deleted: 0,
117+
not_deleted: 2,
118+
},
119+
}, errors: [
120+
{ errors: [{ message: 'Rule does not exist', parameters: {} }] },
121+
{ errors: [{ message: 'Rule does not exist', parameters: {} }] },
122+
],
123+
});
124+
});
125+
126+
it('should get all currently available rules when no IDs are given', async () => {
127+
const getRulesResponse = await clientWithLabs.getRules();
128+
expect(getRulesResponse.data).toBeDefined();
129+
expect(getRulesResponse.data).toContainEqual(...addedRules);
130+
});
131+
132+
it('should get only specified rules when IDs are given', async () => {
133+
const getRulesResponse = await clientWithLabs.getRules(addedRulesId.slice(0, 2));
134+
expect(getRulesResponse.data).toBeDefined();
135+
expect(getRulesResponse.data).toHaveLength(2);
136+
expect(getRulesResponse.data).toContainEqual(...addedRules.slice(0, 2));
137+
expect(getRulesResponse.data).not.toContainEqual(addedRules[2]);
138+
});
139+
140+
});

0 commit comments

Comments
 (0)