Skip to content

Commit

Permalink
Added Twitter Labs filter stream v1 support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
btruhand committed Jul 24, 2020
1 parent 084a68d commit 4d12951
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 17 deletions.
103 changes: 99 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export default class Twitter {
* @returns {Stream}
*/
public stream(resource: string, parameters: object): Stream;

/**
* Creates an instance of the original {@instance Twitter} with labs API capabilities
* @return {TwitterLabs} - a twitter labs instance
*/
public withLabs(): TwitterLabs;

/**
* Add rule for the filter stream API
*
* @param {LabsFilterStreamRule[]} rules a list of rules for the filter stream API
* @param {boolean} [dryRun] optional parameter to mark the request as a dry run
* @returns {Promise<object>} Promise response from Twitter API
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/post-tweets-stream-filter-rules Twitter API}
*/
public static labsFilterStreamRule(value: string, tag?: string): FilterStreamRule;
}

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

type TokenResponse =
| {
oauth_token: OauthToken;
oauth_token_secret: OauthTokenSecret;
oauth_callback_confirmed: 'true';
}
oauth_token: OauthToken;
oauth_token_secret: OauthTokenSecret;
oauth_callback_confirmed: 'true';
}
| { oauth_callback_confirmed: 'false' };

interface AccessTokenResponse {
Expand All @@ -176,3 +192,82 @@ declare class Stream extends EventEmitter {
parse(buffer: Buffer): void;
destroy(): void;
}

export class TwitterLabs extends Twitter {
/**
* Construct the data and headers for an authenticated HTTP request to the Twitter Labs API
* @param {'GET | 'POST' | 'PUT'} method
* @param {'1' | '2'} version
* @param {string} resource - the API endpoint
* @param {object} queryParams - query params object
*/
private _makeLabsRequest(method: 'GET' | 'POST' | 'PUT', version: '1' | '2',
resource: string, queryParams: object): {
requestData: { url: string; method: string };
headers: { Authorization: string } | OAuth.Header;
};

/**
* Add rule for the filter stream API
*
* @param {FilterStreamRule[]} rules a list of rules for the filter stream API
* @param {boolean} [dryRun] optional parameter to mark the request as a dry run
* @returns {Promise<object>} Promise response from Twitter API
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/post-tweets-stream-filter-rules Twitter API}
*/
public addRules(rules: FilterStreamRule[], dryRun?: boolean): Promise<object>

/**
* Get registered rules
*
* @returns {Promise<object>} Promise response from Twitter API
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter-rules Twitter API}
*/
public getRules(...ids: string[]): Promise<object>

/**
* Delete registered rules
*
* @param {string[]} Rule IDs that has been registered
* @param {boolean} [dryRun] optional parameter to mark request as a dry run
* @returns {Promise<object>} Promise response from Twitter API
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter-rules Twitter API}
*/
public deleteRules(ids: string[], dryRun?: boolean): Promise<object>


/**
* Start filter stream using saved rules
*
* @param {{expansions: Expansions[], format: Format, 'place.format': Format,
* 'tweet.format': Format, 'user.format': Format}} [queryParams]
* @returns {Stream} stream object for the filter stream
* @see {@link https://developer.twitter.com/en/docs/labs/filtered-stream/api-reference/get-tweets-stream-filter Twitter API}
*/
filterStream(queryParams?: FilterStreamParams): Promise<object>
}

/**
* Rule structure when adding twitter labs filter stream rules
*/
type FilterStreamRule = { value: string, meta?: string };

/**
* Twitter labs response format
* @see {@link https://developer.twitter.com/en/docs/labs/overview/whats-new/formats About format}
*/
type LabsFormat = 'compact' | 'detailed' | 'default';

/**
* Twitter labs expansions
* @see {@link https://developer.twitter.com/en/docs/labs/overview/whats-new/expansions About expansions}
*/
type LabsExpansion = 'attachment.poll_ids' | 'attachments.media_keys' | 'author_id' | 'entities.mentions.username' | 'geo.place_id'
| 'in_reply_to_user_id' | 'referenced_tweets.id' | 'referenced_tweets.id.author_id';
type FilterStreamParams = {
expansions?: LabsExpansion[],
format?: LabsFormat,
'place.format'?: LabsFormat,
'tweet.format'?: LabsFormat,
'user.format'?: LabsFormat
};
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"scripts": {
"lint": "eslint --fix ./",
"prepare": "microbundle {stream,twitter}.js && bundlesize",
"test": "eslint --fix . && jest --detectOpenHandles",
"test": "eslint --fix . && jest --testPathIgnorePatterns=labs --detectOpenHandles",
"test-labs": "eslint --fix . && jest --testPathPattern=labs --detectOpenHandles",
"release": "npm run -s prepare && npm test && git tag $npm_package_version && git push && git push --tags && npm publish"
},
"husky": {
Expand All @@ -68,4 +69,4 @@
"maxSize": "3 kB"
}
]
}
}
140 changes: 140 additions & 0 deletions test/twitter.labs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
require('dotenv').config();
const Twitter = require('../twitter');

const {
TWITTER_CONSUMER_KEY,
TWITTER_CONSUMER_SECRET,
ACCESS_TOKEN,
ACCESS_TOKEN_SECRET,
} = process.env;

function newClient() {
return new Twitter({
consumer_key: TWITTER_CONSUMER_KEY,
consumer_secret: TWITTER_CONSUMER_SECRET,
access_token_key: ACCESS_TOKEN,
access_token_secret: ACCESS_TOKEN_SECRET,
});
}


describe('LABS - creating with labs', () => {
let client;
let clientWithLabs;
beforeAll(() => {
client = newClient();
clientWithLabs = client.withLabs();
});

it('should create object with all twitter functions', () => {
for (const funcName of Object.getOwnPropertyNames(Twitter.prototype)) {
expect(clientWithLabs[funcName]).toBeDefined();
expect(clientWithLabs[funcName]).toBeInstanceOf(Function);
}
});

it('should create object with all twitter properties', () => {
for (const propertyName of Object.getOwnPropertyNames(client)) {
expect(clientWithLabs[propertyName]).toBeDefined();
expect(clientWithLabs[propertyName]);
}
});
});

describe('LABS - filter stream labs', () => {
let clientWithLabs;
let addedRules;
let addedRulesId;

// create labs instance and add initial rules
beforeAll(async () => {
const bearerToken = await newClient().getBearerToken();
clientWithLabs = new Twitter({ bearer_token: bearerToken.access_token }).withLabs();
const rulesToAdd = [
Twitter.labsFilterStreamRule('twitter'),
Twitter.labsFilterStreamRule('testing'),
Twitter.labsFilterStreamRule('hello'),
];
const response = await clientWithLabs.addRules(rulesToAdd);
addedRules = response.data;
addedRulesId = response.data.map(d => d.id);
});

// delete initialized rules
afterAll(async () => {
await clientWithLabs.deleteRules(addedRulesId);
});

it('should create new rules when adding non-existent rules', async () => {
const rulesToAdd = [Twitter.labsFilterStreamRule('random1'), Twitter.labsFilterStreamRule('random2')];
const addRulesResponse = await clientWithLabs.addRules(rulesToAdd, true);

expect(addRulesResponse).toMatchObject({
data: [
{ value: 'random1', id: expect.any(String) },
{ value: 'random2', id: expect.any(String) },
],
meta: {
summary: {
created: 2,
},
},
});
});

it('should not create new rules when adding existing rules', async () => {
const rulesToAdd = [Twitter.labsFilterStreamRule('twitter'), Twitter.labsFilterStreamRule('testing')];
const addRulesResponse = await clientWithLabs.addRules(rulesToAdd, true);

expect(addRulesResponse).toMatchObject({
meta: {
summary: {
created: 0,
},
},
});
});

it('should delete rules that exist', async () => {
const deleteRulesResponse = await clientWithLabs.deleteRules(addedRulesId, true);

expect(deleteRulesResponse).toMatchObject({
meta: {
summary: {
deleted: addedRulesId.length,
},
},
});
});

it('should be an error when deleting rules that does not exist', async () => {
const deleteRulesResponse = await clientWithLabs.deleteRules(['239197139192', '28319317192'], true);

expect(deleteRulesResponse).toMatchObject({
meta: {
summary: {
deleted: 0,
not_deleted: 2,
},
}, errors: [
{ errors: [{ message: 'Rule does not exist', parameters: {} }] },
{ errors: [{ message: 'Rule does not exist', parameters: {} }] },
],
});
});

it('should get all currently available rules when no IDs are given', async () => {
const getRulesResponse = await clientWithLabs.getRules();
expect(getRulesResponse.data).toBeDefined();
expect(getRulesResponse.data).toContainEqual(...addedRules);
});

it('should get only specified rules when IDs are given', async () => {
const getRulesResponse = await clientWithLabs.getRules(addedRulesId.slice(0, 2));
expect(getRulesResponse.data).toBeDefined();
expect(getRulesResponse.data).toHaveLength(2);
expect(getRulesResponse.data).toContainEqual(...addedRules.slice(0, 2));
expect(getRulesResponse.data).not.toContainEqual(addedRules[2]);
});

});
Loading

0 comments on commit 4d12951

Please sign in to comment.