From ec66743332f7069b5d881607a06d4c6b0c232917 Mon Sep 17 00:00:00 2001 From: Mauricio Robayo Date: Fri, 2 Oct 2020 12:25:09 -0500 Subject: [PATCH 1/4] chore: fix OAuth typos --- __tests__/authorization.js | 4 ++-- __tests__/modules/parameterString.js | 4 ++-- __tests__/modules/signature.js | 4 ++-- __tests__/modules/signatureBaseString.js | 4 ++-- src/authorization/authorization.js | 14 +++++++------- src/authorization/modules/parameterString.js | 10 +++++----- src/authorization/modules/signature.js | 6 +++--- src/authorization/modules/signatureBaseString.js | 4 ++-- src/index.js | 10 +++++----- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/__tests__/authorization.js b/__tests__/authorization.js index 5b439ba..6dd3af9 100644 --- a/__tests__/authorization.js +++ b/__tests__/authorization.js @@ -15,7 +15,7 @@ const queryParams = { include_entities: true } const bodyParams = { status: 'Hello Ladies + Gentlemen, a signed OAuth request!', } -const oauthOptions = { +const oAuthOptions = { api_key: 'xvz1evFS4wEEPTGEFPHBog', api_secret_key: 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw', access_token: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', @@ -29,7 +29,7 @@ it('should return the authorization header string', () => { baseUrl, queryParams, bodyParams, - oauthOptions, + oAuthOptions, }), ).toBe( 'OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", oauth_version="1.0"', diff --git a/__tests__/modules/parameterString.js b/__tests__/modules/parameterString.js index 7b293ec..3e39eda 100644 --- a/__tests__/modules/parameterString.js +++ b/__tests__/modules/parameterString.js @@ -8,7 +8,7 @@ const queryParams = { const bodyParams = { status: 'Hello Ladies + Gentlemen, a signed OAuth request!', } -const oauthOptions = { +const oAuthOptions = { api_key: 'xvz1evFS4wEEPTGEFPHBog', access_token: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', oauth_nonce: 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg', @@ -16,7 +16,7 @@ const oauthOptions = { } it('should return the parameter string', () => { - expect(parameterString(queryParams, bodyParams, oauthOptions)).toBe( + expect(parameterString(queryParams, bodyParams, oAuthOptions)).toBe( 'include_entities=true&oauth_consumer_key=xvz1evFS4wEEPTGEFPHBog&oauth_nonce=kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1318622958&oauth_token=370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb&oauth_version=1.0&status=Hello%20Ladies%20%2B%20Gentlemen%2C%20a%20signed%20OAuth%20request%21', ) }) diff --git a/__tests__/modules/signature.js b/__tests__/modules/signature.js index c09995d..da85955 100644 --- a/__tests__/modules/signature.js +++ b/__tests__/modules/signature.js @@ -6,7 +6,7 @@ const queryParams = { include_entities: true } const bodyParams = { status: 'Hello Ladies + Gentlemen, a signed OAuth request!', } -const oauthOptions = { +const oAuthOptions = { api_key: 'xvz1evFS4wEEPTGEFPHBog', access_token: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', oauth_nonce: 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg', @@ -22,7 +22,7 @@ it('should return the signature', () => { baseUrl, queryParams, bodyParams, - oauthOptions, + oAuthOptions, }), ).toBe('hCtSmYh+iHYCEqBWrE7C7hYmtUk=') }) diff --git a/__tests__/modules/signatureBaseString.js b/__tests__/modules/signatureBaseString.js index 19be367..d8fa3e8 100644 --- a/__tests__/modules/signatureBaseString.js +++ b/__tests__/modules/signatureBaseString.js @@ -10,7 +10,7 @@ const queryParams = { const bodyParams = { status: 'Hello Ladies + Gentlemen, a signed OAuth request!', } -const oauthOptions = { +const oAuthOptions = { api_key: 'xvz1evFS4wEEPTGEFPHBog', access_token: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', oauth_nonce: 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg', @@ -24,7 +24,7 @@ it('should return the parameter string', () => { baseUrl, queryParams, bodyParams, - oauthOptions, + oAuthOptions, }), ).toBe( 'POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521', diff --git a/src/authorization/authorization.js b/src/authorization/authorization.js index 064802e..64aedbf 100644 --- a/src/authorization/authorization.js +++ b/src/authorization/authorization.js @@ -1,27 +1,27 @@ const { randomString, timestamp, percentEncode } = require('./helpers') const { signature } = require('./modules/signature') -exports.authorization = options => { +exports.authorization = (options) => { /* You should be able to see that the header contains 7 key/value pairs, where the keys all begin with the string “oauth_”. For any given Twitter API request, collecting these 7 values and creating a similar header will allow you to specify authorization for the request. */ - const oauthParams = { - oauth_consumer_key: options.oauthOptions.api_key, + const oAuthParams = { + oauth_consumer_key: options.oAuthOptions.api_key, oauth_nonce: randomString(32), oauth_signature: '', oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: timestamp(), - oauth_token: options.oauthOptions.access_token, + oauth_token: options.oAuthOptions.access_token, oauth_version: '1.0', } /* Generate signature */ - oauthParams.oauth_signature = signature({ + oAuthParams.oauth_signature = signature({ ...options, - oauthOptions: Object.assign(options.oauthOptions, oauthParams), + oAuthOptions: Object.assign(options.oAuthOptions, oAuthParams), }) /* @@ -34,7 +34,7 @@ exports.authorization = options => { 5. Append a double quote ‘”’ to DST. 6. If there are key/value pairs remaining, append a comma ‘,’ and a space ‘ ‘ to DST. */ - const outputString = `OAuth ${Object.entries(oauthParams) + const outputString = `OAuth ${Object.entries(oAuthParams) .map(([key, value]) => `${percentEncode(key)}="${percentEncode(value)}"`) .join(', ')}` diff --git a/src/authorization/modules/parameterString.js b/src/authorization/modules/parameterString.js index 88d0f10..24873f0 100644 --- a/src/authorization/modules/parameterString.js +++ b/src/authorization/modules/parameterString.js @@ -22,16 +22,16 @@ function encodeParams(params) { }, {}) } -exports.parameterString = (queryParams, bodyParams, oauthOptions) => { +exports.parameterString = (queryParams, bodyParams, oAuthOptions) => { /* Collecting parameters */ const params = Object.assign(queryParams, bodyParams, { - oauth_consumer_key: oauthOptions.api_key, - oauth_nonce: oauthOptions.oauth_nonce, + oauth_consumer_key: oAuthOptions.api_key, + oauth_nonce: oAuthOptions.oauth_nonce, oauth_signature_method: 'HMAC-SHA1', - oauth_timestamp: oauthOptions.oauth_timestamp, - oauth_token: oauthOptions.access_token, + oauth_timestamp: oAuthOptions.oauth_timestamp, + oauth_token: oAuthOptions.access_token, oauth_version: '1.0', }) /* diff --git a/src/authorization/modules/signature.js b/src/authorization/modules/signature.js index 2ff87d7..1e3ada5 100644 --- a/src/authorization/modules/signature.js +++ b/src/authorization/modules/signature.js @@ -2,10 +2,10 @@ const crypto = require('crypto') const { signatureBaseString } = require('./signatureBaseString') const { percentEncode } = require('../helpers') -exports.signature = options => { +exports.signature = (options) => { const baseString = signatureBaseString(options) - const consumerSecret = percentEncode(options.oauthOptions.api_secret_key) - const tokenSecret = percentEncode(options.oauthOptions.access_token_secret) + const consumerSecret = percentEncode(options.oAuthOptions.api_secret_key) + const tokenSecret = percentEncode(options.oAuthOptions.access_token_secret) const signingKey = `${consumerSecret}&${tokenSecret}` const outputString = crypto .createHmac('sha1', signingKey) diff --git a/src/authorization/modules/signatureBaseString.js b/src/authorization/modules/signatureBaseString.js index 78d1ec9..f88f863 100644 --- a/src/authorization/modules/signatureBaseString.js +++ b/src/authorization/modules/signatureBaseString.js @@ -6,7 +6,7 @@ exports.signatureBaseString = ({ baseUrl, queryParams, bodyParams, - oauthOptions, + oAuthOptions, }) => { /* 1. Convert the HTTP Method to uppercase and set the output string equal to this value. @@ -15,7 +15,7 @@ exports.signatureBaseString = ({ 4. Append the ‘&’ character to the output string. 5. Percent encode the parameter string and append it to the output string. */ - const paramString = parameterString(queryParams, bodyParams, oauthOptions) + const paramString = parameterString(queryParams, bodyParams, oAuthOptions) const outputString = `${requestMethod.toUpperCase()}&${percentEncode( baseUrl, )}&${percentEncode(paramString)}` diff --git a/src/index.js b/src/index.js index 5e4a272..531f16f 100644 --- a/src/index.js +++ b/src/index.js @@ -42,16 +42,16 @@ function buildHttpsOptions(url, options) { function request(httpsOptions, body) { return new Promise((resolve, reject) => { - const req = https.request(httpsOptions, res => { + const req = https.request(httpsOptions, (res) => { let data = '' - res.on('data', _data => { + res.on('data', (_data) => { data += _data }) res.on('end', () => { resolve(JSON.parse(data)) }) }) - req.on('error', error => { + req.on('error', (error) => { reject(error) }) req.write(body) @@ -59,7 +59,7 @@ function request(httpsOptions, body) { }) } -module.exports = oauthOptions => ({ +module.exports = (oAuthOptions) => ({ subdomain = 'api', endpoint, requestMethod = 'GET', @@ -75,7 +75,7 @@ module.exports = oauthOptions => ({ baseUrl, queryParams, bodyParams, - oauthOptions, + oAuthOptions, }) return request(httpsOptions, body) } From 88260eed41d8f1dc7efbeb2f95584981bd2aebaf Mon Sep 17 00:00:00 2001 From: Mauricio Robayo Date: Fri, 2 Oct 2020 19:14:38 -0500 Subject: [PATCH 2/4] feat: add type definitions closes #43 --- examples/search-tweets.ts | 27 +++++++++++++++++++++++++++ package.json | 1 + src/index.d.ts | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 examples/search-tweets.ts create mode 100644 src/index.d.ts diff --git a/examples/search-tweets.ts b/examples/search-tweets.ts new file mode 100644 index 0000000..404d434 --- /dev/null +++ b/examples/search-tweets.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-console */ +// https://developer.twitter.com/en/apps/ +import twitterize, { OAuthOptions, RequestOptions } from '../src' + +interface Tweets { + statuses: [] + search_metadata: Record +} + +const oAuthOptions: OAuthOptions = { + api_key: process.env.TWITTER_API_KEY || '', + api_secret_key: process.env.TWITTER_API_SECRET_KEY || '', + access_token: process.env.TWITTER_ACCESS_TOKEN || '', + access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET || '', +} + +const twitterizeRequest = twitterize(oAuthOptions) + +const requestOptions: RequestOptions = { + requestMethod: 'GET', + endpoint: '/search/tweets.json', + queryParams: { q: 'twitter bot' }, +} + +twitterizeRequest(requestOptions) + .then((data) => console.log(data.search_metadata)) + .catch((error) => console.log(error)) diff --git a/package.json b/package.json index e1d1a94..a653221 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "twitterize", "version": "0.0.0-development", "main": "src/index.js", + "types": "src/index.d.ts", "scripts": { "lint": "eslint src", "format": "prettier --write src/**/*.{js,ts,css,less,scss,vue,json,gql,md,yml,yaml}", diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..03cb298 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,18 @@ +export type OAuthOptions = { + api_key: string + api_secret_key: string + access_token: string + access_token_secret: string +} + +export type RequestOptions = { + subdomain?: string + endpoint: string + requestMethod?: string + queryParams?: Record + bodyParams?: Record +} + +export default function ( + oAuthOptions: OAuthOptions, +): (requestOptions: RequestOptions) => Promise From 6a44c89531da478cab1a6ffc67f315501af03681 Mon Sep 17 00:00:00 2001 From: Mauricio Robayo Date: Fri, 2 Oct 2020 19:23:32 -0500 Subject: [PATCH 3/4] docs(typescript): add script to run ts example --- package-lock.json | 6 +++++ package.json | 4 ++- tsconfig.json | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 3c4542e..aed3017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8506,6 +8506,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/package.json b/package.json index a653221..69ff88c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "format": "prettier --write src/**/*.{js,ts,css,less,scss,vue,json,gql,md,yml,yaml}", "test": "jest --coverage", "example:search": "run.env node examples/search-tweets.js", + "example:search-ts": "run.env npx ts-node examples/search-tweets.ts", "example:post": "run.env node examples/post-tweet.js", "example:upload": "run.env node examples/upload-tweet.js" }, @@ -30,7 +31,8 @@ "jest": "^26.4.2", "lint-staged": "^10.4.0", "prettier": "^2.1.2", - "run.env": "^1.1.0" + "run.env": "^1.1.0", + "typescript": "^4.0.3" }, "dependencies": {}, "description": "Send authorized requests to the Twitter API.", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1c62c46 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} From 957640d838c12de4799d97223dd4911fd525630f Mon Sep 17 00:00:00 2001 From: Mauricio Robayo Date: Fri, 2 Oct 2020 19:27:42 -0500 Subject: [PATCH 4/4] docs(readme): include TypeScript docs --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bce035e..c9808ed 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,10 @@ npm run example:post npm run example:upload ``` +## TypeScript + +Type definitions are included. A TypeScript example is provided [here](./examples/search-tweets.ts), you can run it with `npm run example:search-ts`. + ## Twittter documentation - [Authentication](https://developer.twitter.com/en/docs/basics/authentication/overview/oauth)