├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── other-issue.md
└── workflows
│ ├── CI.yml
│ └── publish.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── changelog.md
├── doc
├── auth.md
├── basics.md
├── errors.md
├── examples.md
├── helpers.md
├── http-wrappers.md
├── paginators.md
├── plugins.md
├── rate-limiting.md
├── streaming.md
├── v1.md
└── v2.md
├── package-lock.json
├── package.json
├── src
├── ads-sandbox
│ ├── client.ads-sandbox.read.ts
│ ├── client.ads-sandbox.ts
│ └── client.ads-sandbox.write.ts
├── ads
│ ├── client.ads.read.ts
│ ├── client.ads.ts
│ └── client.ads.write.ts
├── client-mixins
│ ├── form-data.helper.ts
│ ├── oauth1.helper.ts
│ ├── oauth2.helper.ts
│ ├── request-handler.helper.ts
│ ├── request-maker.mixin.ts
│ └── request-param.helper.ts
├── client.base.ts
├── client.subclient.ts
├── client
│ ├── index.ts
│ ├── readonly.ts
│ └── readwrite.ts
├── globals.ts
├── helpers.ts
├── index.ts
├── paginators
│ ├── TwitterPaginator.ts
│ ├── dm.paginator.v1.ts
│ ├── dm.paginator.v2.ts
│ ├── followers.paginator.v1.ts
│ ├── friends.paginator.v1.ts
│ ├── index.ts
│ ├── list.paginator.v1.ts
│ ├── list.paginator.v2.ts
│ ├── mutes.paginator.v1.ts
│ ├── paginator.v1.ts
│ ├── tweet.paginator.v1.ts
│ ├── tweet.paginator.v2.ts
│ ├── user.paginator.v1.ts
│ ├── user.paginator.v2.ts
│ └── v2.paginator.ts
├── plugins
│ └── helpers.ts
├── settings.ts
├── stream
│ ├── TweetStream.ts
│ ├── TweetStreamEventCombiner.ts
│ └── TweetStreamParser.ts
├── test
│ └── utils.ts
├── types
│ ├── auth.types.ts
│ ├── client.types.ts
│ ├── entities.types.ts
│ ├── errors.types.ts
│ ├── index.ts
│ ├── plugins
│ │ ├── client.plugins.types.ts
│ │ └── index.ts
│ ├── request-maker.mixin.types.ts
│ ├── responses.types.ts
│ ├── shared.types.ts
│ ├── v1
│ │ ├── dev-utilities.v1.types.ts
│ │ ├── dm.v1.types.ts
│ │ ├── entities.v1.types.ts
│ │ ├── geo.v1.types.ts
│ │ ├── index.ts
│ │ ├── list.v1.types.ts
│ │ ├── streaming.v1.types.ts
│ │ ├── trends.v1.types.ts
│ │ ├── tweet.v1.types.ts
│ │ └── user.v1.types.ts
│ └── v2
│ │ ├── community.v2.types.ts
│ │ ├── dm.v2.types.ts
│ │ ├── index.ts
│ │ ├── list.v2.types.ts
│ │ ├── media.v2.types.ts
│ │ ├── shared.v2.types.ts
│ │ ├── spaces.v2.types.ts
│ │ ├── streaming.v2.types.ts
│ │ ├── tweet.definition.v2.ts
│ │ ├── tweet.v2.types.ts
│ │ └── user.v2.types.ts
├── v1
│ ├── client.v1.read.ts
│ ├── client.v1.ts
│ ├── client.v1.write.ts
│ └── media-helpers.v1.ts
├── v2-labs
│ ├── client.v2.labs.read.ts
│ ├── client.v2.labs.ts
│ └── client.v2.labs.write.ts
└── v2
│ ├── client.v2.read.ts
│ ├── client.v2.ts
│ ├── client.v2.write.ts
│ └── includes.v2.helper.ts
├── test
├── account.v1.test.ts
├── assets
│ ├── bbb.mp4
│ ├── lolo.jpg
│ └── pec.gif
├── auth.test.ts
├── dm.v1.test.ts
├── list.v1.test.ts
├── media-upload.v1..test.ts
├── media-upload.v2.test.ts
├── plugin.test.ts
├── space.v2.test.ts
├── stream.test.ts
├── tweet.v1.test.ts
├── tweet.v2.test.ts
├── user.v1.test.ts
└── user.v2.test.ts
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | parser: '@typescript-eslint/parser',
7 | plugins: ['@typescript-eslint'],
8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
9 | // add your custom rules here
10 | rules: {
11 | semi: ['error', 'always'],
12 | 'no-extra-semi': 'error',
13 | 'no-extra-parens': 'off',
14 | 'comma-dangle': ['error', 'always-multiline'],
15 | 'space-before-function-paren': ['error', {
16 | anonymous: 'always',
17 | named: 'never',
18 | asyncArrow: 'always',
19 | }],
20 | 'func-call-spacing': ['error', 'never'],
21 | 'no-console': 'off',
22 | 'no-unused-expression': 'off',
23 | 'no-useless-constructor': 'off',
24 | 'arrow-parens': 'off',
25 | 'no-use-before-define': 'off',
26 | 'no-return-assign': 'off',
27 | 'quotes': ['error', 'single'],
28 | 'member-access': 'off',
29 | 'member-ordering': 'off',
30 | 'object-literal-sort-keys': 'off',
31 | 'no-trailing-spaces': 'error',
32 | '@typescript-eslint/no-inferrable-types': 'off',
33 | '@typescript-eslint/explicit-module-boundary-types': 'off',
34 | '@typescript-eslint/no-explicit-any': 'off',
35 | '@typescript-eslint/no-non-null-assertion': 'off',
36 | '@typescript-eslint/no-extra-parens': ['off'],
37 | '@typescript-eslint/ban-types': 'off',
38 | '@typescript-eslint/no-unused-vars': ['error', {
39 | varsIgnorePattern: '^_',
40 | }],
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[bug]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Please indicate all steps that lead to this bug:
15 | 1. Request client setup (login method, OAuth2 scopes if applicable...)
16 | 2. Endpoint used or code example of what's happening wrong
17 | 3. Error stack trace, and if possible, error content (`err.toJSON()` when `err` is the caught error; take care of removing authentication HTTP headers)
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Version**
23 | - Node.js version
24 | - Lib version
25 | - OS (especially if you use Windows)
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/other-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Other issue
3 | about: A problem that isn't a bug or a feature request?
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | *Note: For questions about how to use an endpoint, or problems related to Twitter API than the lib itself, please use the GitHub Discussions instead of opening a new issue*.
11 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: push
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 |
8 | strategy:
9 | matrix:
10 | node-version: [20.x]
11 | max-parallel: 1 # the streaming API can be called only once at a time
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - run: npm ci
20 | - run: npm run build
21 | - run: npm test
22 | env:
23 | CONSUMER_TOKEN: ${{ secrets.CONSUMER_TOKEN }}
24 | CONSUMER_SECRET: ${{ secrets.CONSUMER_SECRET }}
25 | OAUTH_TOKEN: ${{ secrets.OAUTH_TOKEN }}
26 | OAUTH_SECRET: ${{ secrets.OAUTH_SECRET }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: npm publish
2 | on:
3 | release:
4 | types: [created]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | # Setup .npmrc file to publish to npm
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: '14.x'
14 | registry-url: 'https://registry.npmjs.org'
15 | - run: npm install
16 | # Publish to npm
17 | - run: npm publish --access public
18 | env:
19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built js file
2 | dist/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | # TypeScript cache
16 | *.tsbuildinfo
17 |
18 | # dotenv environment variables file
19 | .env
20 |
21 | # IDE
22 | .idea
23 | .vscode
24 |
25 | *.DS_Store
26 |
27 | # TypeScript auto-generated doc
28 | tsdocs/
29 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | ./src/
2 | ./test/
3 | ./tsdocs/
4 | ./dist/test/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Twitter API v2
2 |
3 | [](https://github.com/PLhery/node-twitter-api-v2)
4 | [](https://github.com/PLhery/node-twitter-api-v2/actions/workflows/CI.yml)
5 | [](https://bundlephobia.com/package/twitter-api-v2)
6 |
7 | Strongly typed, full-featured, light, versatile yet powerful Twitter API v1.1 and v2 client for Node.js.
8 |
9 | Main maintainer: [@alkihis](https://github.com/alkihis) -
10 |
11 | ## Important Note
12 |
13 | Twitter will significantly reduce its API capabilities by end of April ([see this thread](https://x.com/TwitterDev/status/1641222782594990080)).
14 |
15 | This change has major implications, and as a result, this library may no longer be maintained.
16 |
17 | We are disappointed and discouraged by the recent turn of events at Twitter since the takeover by Elon Musk. We are saddened to see that much of the hard work of the past few years on the API, led by an amazing team including @andypiper, has been shelved.
18 |
19 | For a more detailed explanation, please see [this discussion](https://github.com/PLhery/node-twitter-api-v2/discussions/459).
20 |
21 | ## Highlights
22 |
23 | ✅ **Ready for v2 and good ol' v1.1 Twitter API**
24 |
25 | ✅ **Light: No dependencies, 23kb minified+gzipped**
26 |
27 | ✅ **Bundled types for request parameters and responses**
28 |
29 | ✅ **Streaming support**
30 |
31 | ✅ **Pagination utils**
32 |
33 | ✅ **User-context authentication with OAuth2**
34 |
35 | ✅ **Media upload helpers**
36 |
37 | ## How to use
38 |
39 | Install it through your favorite package manager:
40 | ```bash
41 | yarn add twitter-api-v2
42 | # or
43 | npm i twitter-api-v2
44 | ```
45 |
46 | Here's a quick example of usage:
47 |
48 | ```ts
49 | import { TwitterApi } from 'twitter-api-v2';
50 |
51 | // Instantiate with desired auth type (here's Bearer v2 auth)
52 | const twitterClient = new TwitterApi('');
53 |
54 | // Tell typescript it's a readonly app
55 | const readOnlyClient = twitterClient.readOnly;
56 |
57 | // Play with the built in methods
58 | const user = await readOnlyClient.v2.userByUsername('plhery');
59 | await twitterClient.v2.tweet('Hello, this is a test.');
60 | // You can upload media easily!
61 | await twitterClient.v1.uploadMedia('./big-buck-bunny.mp4');
62 | ```
63 |
64 | ## Why?
65 |
66 | Sometimes, you just want to quickly bootstrap an application using the Twitter API.
67 | Even though there are a lot of libraries available on the JavaScript ecosystem, they usually just
68 | provide wrappers around HTTP methods, and some of them are bloated with many dependencies.
69 |
70 | `twitter-api-v2` is meant to provide full endpoint wrapping, from method name to response data,
71 | using descriptive typings for read/write/DMs rights, request parameters and response payload.
72 |
73 | A small feature comparison with other libs:
74 |
75 | | Package | API version(s) | Response typings | Media helpers | Pagination | Subdeps | Size (gzip) | Install size |
76 | | -------------- |---------------------| ---------------- | ------------- | ---------- | --------------- | -------------:| -------------:|
77 | | twitter-api-v2 | v1.1, v2, labs, ads | ✅ | ✅ | ✅ | 0 | ~23 kB | [](https://packagephobia.com/result?p=twitter-api-v2) |
78 | | twit | v1.1 | ❌ | ✅ | ❌ | 51 | ~214.5 kB | [](https://packagephobia.com/result?p=twit) |
79 | | twitter | v1.1 | ❌ | ❌ | ❌ | 50 | ~182.1 kB | [](https://packagephobia.com/result?p=twitter) |
80 | | twitter-lite | v1.1, v2 | ❌ | ❌ | ❌ | 4 | ~5.3 kB | [](https://packagephobia.com/result?p=twitter-lite) |
81 | | twitter-v2 | v2 | ❌ | ❌ | ❌ | 7 | ~4.5 kB | [](https://packagephobia.com/result?p=twitter-v2) |
82 |
83 | ## Features
84 |
85 | Here's everything `twitter-api-v2` can do:
86 |
87 | ### Basics:
88 | - Support for v1.1 and **v2 of Twitter API**
89 | - Make signed HTTP requests to Twitter with every auth type: **OAuth 1.0a**, **OAuth2** (even brand new user context OAuth2!) and **Basic** HTTP Authorization
90 | - Helpers for numerous HTTP request methods (`GET`, `POST`, `PUT`, `DELETE` and `PATCH`),
91 | that handle query string parse & format, automatic body formatting and more
92 | - High-class support for stream endpoints, with easy data consumption and auto-reconnect on stream errors
93 |
94 | ### Request helpers:
95 | - Automatic paginator for endpoints like user and tweet timelines,
96 | allowing payload consumption with modern asynchronous iterators until your rate-limit is hit
97 | - Convenient methods for authentication - generate auth links and ask for tokens to your users
98 | - Media upload with API v1.1, including **long video & subtitles support**, automatic media type detection,
99 | **chunked upload** and support for **concurrent uploads**
100 | - Dedicated methods that wraps API v1.1 & v2 endpoints, with **typed arguments** and fully **typed responses**
101 | - Typed errors, meaningful error messages, error enumerations for both v1.1 and v2
102 |
103 | ### Type-safe first:
104 | - **Typings for tweet, user, media entities (and more) are bundled!**
105 | - Type-safe wrapping of dedicated methods in 3 right level: *DM*/*Read-write*/*Read-only* (just like Twitter API do!) -
106 | you can declare a read-only client - you will only see the methods associated with read-only endpoints
107 |
108 | And last but not least, fully powered by native `Promise`s.
109 |
110 | ## Documentation
111 |
112 | Learn how to use the full potential of `twitter-api-v2`.
113 |
114 | - Get started
115 | - [Create a client and make your first request](./doc/basics.md)
116 | - [Handle Twitter authentication flows](./doc/auth.md)
117 | - [Explore some examples](./doc/examples.md)
118 | - [Use and create plugins](./doc/plugins.md)
119 | - Use endpoints wrappers — ensure typings of request & response
120 | - [Available endpoint wrappers for v1.1 API](./doc/v1.md)
121 | - [Available endpoint wrappers for v2 API](./doc/v2.md)
122 | - [Use Twitter streaming endpoints (v1.1 & v2)](./doc/streaming.md)
123 | - Deep diving into requests
124 | - [Use direct HTTP-method wrappers](./doc/http-wrappers.md)
125 | - [Use rate limit helpers](./doc/rate-limiting.md)
126 | - [Handle errors](./doc/errors.md)
127 | - [Master `twitter-api-v2` paginators](./doc/paginators.md)
128 | - [Discover available helpers](./doc/helpers.md)
129 |
130 | ## Plugins
131 |
132 | Official plugins for `twitter-api-v2`:
133 | - [`@twitter-api-v2/plugin-token-refresher`](https://www.npmjs.com/package/@twitter-api-v2/plugin-token-refresher): Handle OAuth 2.0 (user-context) token refreshing for you
134 | - [`@twitter-api-v2/plugin-rate-limit`](https://www.npmjs.com/package/@twitter-api-v2/plugin-rate-limit): Access and store automatically rate limit data
135 | - [`@twitter-api-v2/plugin-cache-redis`](https://www.npmjs.com/package/@twitter-api-v2/plugin-cache-redis): Store responses in a Redis store and serve cached responses
136 |
137 | See [how to use plugins here](./doc/plugins.md).
138 |
--------------------------------------------------------------------------------
/doc/basics.md:
--------------------------------------------------------------------------------
1 | # Basics
2 |
3 | > *For convenience, all the code examples in this documentation will be presented in TypeScript.*
4 | > Feel free to convert them to regular JavaScript. If you don't use a transpiler,
5 | > you might need to replace `import`s by `require()` calls.
6 |
7 | ## Client basics
8 |
9 | ### Create a client
10 |
11 | Import the default export/`TwitterApi` variable from `twitter-api-v2` module.
12 |
13 | **Developer note:** Default export is a TypeScript-CommonJS-wrapped default export —
14 | it isn't a regular ECMA module default export. See example below.
15 |
16 | ```ts
17 | // This will ONLY work with TypeScript on module: "commonjs"
18 | import TwitterApi from 'twitter-api-v2';
19 |
20 | // This will work on TypeScript (with commonJS and ECMA)
21 | // AND with Node.js in ECMA mode (.mjs files, type: "module" in package.json)
22 | import { TwitterApi } from 'twitter-api-v2';
23 |
24 | // This will work with Node.js on CommonJS mode (TypeScript or not)
25 | const { TwitterApi } = require('twitter-api-v2');
26 | ```
27 |
28 | Instantiate with your wanted authentication method.
29 |
30 | ```ts
31 | // OAuth 1.0a (User context)
32 | const userClient = new TwitterApi({
33 | appKey: 'consumerAppKey',
34 | appSecret: 'consumerAppSecret',
35 | // Following access tokens are not required if you are
36 | // at part 1 of user-auth process (ask for a request token)
37 | // or if you want a app-only client (see below)
38 | accessToken: 'accessOAuthToken',
39 | accessSecret: 'accessOAuthSecret',
40 | });
41 |
42 | // OAuth2 (app-only or user context)
43 | // Create a client with an already known bearer token
44 | const appOnlyClient = new TwitterApi('bearerToken');
45 | // OR - you can also create a app-only client from your consumer keys -
46 | const appOnlyClientFromConsumer = await userClient.appLogin();
47 | ```
48 |
49 | Your client is now ready.
50 |
51 | ### Select your right level
52 |
53 | If you already know your right limit (that you've selected when you've created your app on Twitter Dev portal),
54 | you can choose the right sub-client:
55 |
56 | - `DMs`: Nothing to do, default level
57 | - `Read-write`: `rwClient = client.readWrite`
58 | - `Read-only`: `roClient = client.readOnly`
59 |
60 | ## Authentication
61 |
62 | Please see [Authentication part](./auth.md) of the doc.
63 |
64 | ### Get current user
65 |
66 | #### v1 API
67 |
68 | If you want to access currently the logged user inside v1 API (= you're logged with OAuth 1.0a/OAuth2 user-context),
69 | you can use the method `.currentUser()`.
70 |
71 | This a shortcut to `.v1.verifyCredentials()` with a **cache that store user to avoid multiple API calls**.
72 | Its returns a `UserV1` object.
73 |
74 | #### v2 API
75 |
76 | If you want to access the currently logged user inside v2 API,
77 | you can use the method `.currentUserV2()`.
78 |
79 | This a shortcut to `.v2.me()` with a **cache that store user to avoid multiple API calls**.
80 | Its returns a `UserV2Result` object.
81 |
82 | ## Use the versioned API clients - URL prefixes
83 |
84 | By default, `twitter-api-v2` doesn't know which version of the API you want to use (because it supports both!).
85 |
86 | For this reason, we allow you to choose which version you want to use: `v1` or `v2`!
87 | ```ts
88 | const v1Client = client.v1;
89 | const v2Client = client.v2;
90 |
91 | // We also think of users who test v2 labs endpoints :)
92 | const v2LabsClient = client.v2.labs;
93 | ```
94 |
95 | Using the versioned client **auto-prefix requests** with default prefixes
96 | (for v1: `https://api.x.com/1.1/`, for v2: `https://api.x.com/2/`,
97 | for labs v2: `https://api.x.com/labs/2/`)
98 | and this gives you access to endpoint-wrapper methods!
99 |
100 | ## Use the endpoint-wrapper methods
101 |
102 | See the [documentation for v1 client API](./v1.md) or [documentation for v2 client API](./v2.md).
103 |
104 | ## Make requests behind a proxy
105 |
106 | If your network connection is behind a proxy and you are unable to make requests with the default configuration, you can use a custom HTTP agent to configure this behavior.
107 |
108 | ```ts
109 | // Note: this package is an external package, it isn't bundled with Node.
110 | import * as HttpProxyAgent from 'https-proxy-agent';
111 |
112 | // HTTPS proxy to connect to
113 | // twitter-api-v2 will always use HTTPS
114 | const proxy = process.env.HTTP_PROXY || 'https://1.1.1.1:3000';
115 |
116 | // create an instance of the `HttpProxyAgent` class with the proxy server information
117 | const httpAgent = new HttpProxyAgent(proxy);
118 |
119 | // Instantiate helper with the agent
120 | const client = new TwitterApi('', { httpAgent });
121 | ```
122 |
--------------------------------------------------------------------------------
/doc/errors.md:
--------------------------------------------------------------------------------
1 | # Error handling
2 |
3 | When a request fails, you can catch a `ApiRequestError`, a `ApiPartialResponseError` or a `ApiResponseError` object (all instances of `Error`), that contain useful information about whats happening.
4 |
5 | - An `ApiRequestError` happens when the request **failed to sent** (network error, bad URL...).
6 | - An `ApiPartialResponseError` happens the response has been partially sent, but the connection is closed (by you, the OS or Twitter).
7 | - An `ApiResponseError` happens when **Twitter replies with an error**.
8 |
9 | Some properties are common for both objects:
10 | - `error` is `true`
11 | - `type` contains either `ETwitterApiError.Request`, `ETwitterApiError.PartialResponse` or `ETwitterApiError.Response` (depending of error)
12 | - `request` containing node's raw `ClientRequest` instance
13 |
14 | ## Specific properties of `ApiRequestError`
15 | - `requestError`, an instance of `Error` that has been thrown through `request.on('error')` handler
16 |
17 | ## Specific properties of `ApiPartialResponseError`
18 | - `responseError`, an instance of `Error` that has been thrown by `response.on('error')`, or by the tentative of parsing the result of a partial response
19 | - `response`, containing raw node's `IncomingMessage` instance
20 | - `rawContent`, containing all the chunks received from distant server
21 |
22 | ## Specific properties of `ApiResponseError`
23 | - `data`, containing parsed Twitter response data (type of `TwitterApiErrorData`)
24 | - `code` is a `number` containing the HTTP error code (`401`, `404`, ...)
25 | - `response`, containing raw node's `IncomingMessage` instance
26 | - `headers`, containing `IncomingHttpHeaders`
27 | - `rateLimit` (can be undefined or `TwitterRateLimit`), containing parsed rate limit headers (if any)
28 | - (getter) `errors`, direct access of parsed Twitter errors (`(ErrorV1 | ErrorV2)[]` or `undefined`)
29 | - (getter) `rateLimitError`, `true` if this error is fired because a rate limit has been hit
30 | - (getter) `isAuthError`, `true` if this error is fired because logged user cannot do this action (invalid token, invalid app rights...)
31 |
32 | ## Specific methods of `ApiResponseError`
33 | - `hasErrorCode(code: number | EApiV1ErrorCode | EApiV2ErrorCode)`: Tells if given Twitter error code is present in error response
34 |
35 | ## Compression
36 |
37 | Requests (that aren't used for streaming) natively support `gzip`/`deflate` compression.
38 | If it causes issues, you can force compression to be disabled:
39 |
40 | ```ts
41 | const client = new TwitterApi(tokens, { compression: false });
42 | ```
43 |
44 | ## Advanced: Debug a single request
45 |
46 | If you want to debug a single request made through direct HTTP handlers `.get`/`.post`/`.delete`,
47 | you can use an additional property named `requestEventDebugHandler`.
48 |
49 | ```ts
50 | client.v1.get(
51 | 'statuses/user_timeline.json',
52 | { user_id: 10, count: 200 },
53 | { requestEventDebugHandler: (eventType, data) => console.log('Event', eventType, 'with data', data) },
54 | )
55 | ```
56 |
57 | It takes a function of type `(event: TRequestDebuggerHandlerEvent, data?: any) => void` where available events are:
58 | ```ts
59 | type TRequestDebuggerHandlerEvent = 'abort' | 'socket' | 'socket-error' | 'socket-connect'
60 | | 'socket-close' | 'socket-end' | 'socket-lookup' | 'socket-timeout' | 'request-error'
61 | | 'response' | 'response-aborted' | 'response-error' | 'response-close' | 'response-end';
62 | ```
63 |
64 | `data` parameter associated to events:
65 | - `abort`: None / `abort` event of `request`
66 | - `socket`: `{ socket: Socket }` / `request.socket` object, when it is available through `request.on('socket')`
67 | - `socket-error`: `{ socket: Socket, error: Error }` / `error` event of `request.socket`
68 | - `socket-connect`: `{ socket: Socket }` / `connect` event of `request.socket`
69 | - `socket-close`: `{ socket: Socket, withError: boolean }` / `close` event of `request.socket`
70 | - `socket-end`: `{ socket: Socket }` / `end` event of `request.socket`
71 | - `socket-lookup`: `{ socket: Socket, data: [err: Error?, address: string, family: string | number, host: string] }` / `lookup` event of `request.socket`
72 | - `socket-timeout`: `{ socket: Socket }` / `timeout` event of `request.socket`
73 | - `request-error`: `{ requestError: Error }` / `error` event of `request`
74 | - `response`: `{ res: IncomingMessage }` / `response` object, when it is available through `request.on('response')`
75 | - `response-aborted`: `{ error?: Error }` / `aborted` event of `response`
76 | - `response-error`: `{ error: Error }` / `error` event of `response`
77 | - `response-close`: `{ data: string }` (raw response data) / `close` event of `response`
78 | - `response-end`: None / `end` event of `response`
79 |
80 | ## Advanced: Debug all made requests
81 |
82 | If you keep obtaining errors and you don't know how to obtain the response data, or you want to see exactly what have been sent to Twitter,
83 | you can enable the debug mode:
84 | ```ts
85 | import { TwitterApiV2Settings } from 'twitter-api-v2';
86 |
87 | TwitterApiV2Settings.debug = true;
88 | ```
89 |
90 | By default, **all requests and responses** will be printed to console.
91 |
92 | You can customize the output by implementing your own debug logger:
93 | ```ts
94 | // Here's the default logger:
95 | TwitterApiV2Settings.logger = {
96 | log: (msg, payload) => console.log(msg, payload),
97 | };
98 |
99 | // .logger follows this interface:
100 | interface ITwitterApiV2SettingsLogger {
101 | log(message: string, payload?: any): void;
102 | }
103 |
104 | // An example for a file logger
105 | import * as fs from 'fs';
106 | import * as util from 'util';
107 |
108 | const destination = fs.createWriteStream('requests.log', { flags: 'a' });
109 |
110 | TwitterApiV2Settings.logger = {
111 | log: (msg, payload) => {
112 | if (payload) {
113 | const strPayload = util.inspect(payload);
114 | destination.write(msg + ' ' + strPayload);
115 | } else {
116 | destination.write(msg);
117 | }
118 | },
119 | };
120 | ```
121 |
--------------------------------------------------------------------------------
/doc/http-wrappers.md:
--------------------------------------------------------------------------------
1 | # HTTP wrappers & custom signed requests
2 |
3 | **Note about URL prefixes**: `.v1` and `.v2` accessors are here for two reasons;
4 | let you access some endpoint wrappers,
5 | **and** auto-prefix your requests when you use HTTP-method wrappers.
6 |
7 | Check [in the basics](./basics.md) what are the defined URL prefixes for `.v1` and `.v2`.
8 | If your endpoint request URL *doesn't start with those prefixes*, you must specify it manually!
9 |
10 | ## Use the direct HTTP methods wrappers
11 |
12 | If the endpoint-wrapper for your request has not been made yet, don't leave!
13 | You can make requests on your own!
14 |
15 | - `.get` and `.delete`, that takes `(partialUrl: string, query?: TRequestQuery, requestSettings?: TGetClientRequestArgs)` in parameters
16 | - `.post`, `.put` and `.patch` that takes `(partialUrl: string, body?: TRequestBody, requestSettings?: TGetClientRequestArgs)` in parameters
17 |
18 | ```ts
19 | // Don't forget the .json in most of the v1 endpoints!
20 | client.v1.get('statuses/user_timeline.json', { user_id: 14 });
21 |
22 | // or, for v2
23 | client.v2.get('users/14/tweets');
24 | ```
25 |
26 | ### Specify request args
27 |
28 | Sometimes, you need to customize request settings (API prefix, body mode, response mode). You can pass request options through the **third parameter** of HTTP methods wrappers.
29 | ```ts
30 | // [prefix]
31 | // Customize API prefix (prefix that will be prepended to URL in first argument)
32 | client.v1.post('media/upload.json', { media: Buffer.alloc(1024) }, { prefix: 'https://upload.x.com/1.1/' })
33 |
34 | // [forceBodyMode]
35 | // Customize body mode (if automatic body detection don't work)
36 | // Body mode can be 'url', 'form-data', 'json' or 'raw' [only with buffers]
37 | client.v1.post('statuses/update.json', { status: 'Hello' }, { forceBodyMode: 'url' })
38 |
39 | // [fullResponse]
40 | // Obtain the full response object with rate limits
41 | const res = await client.v1.get('statuses/home_timeline.json', undefined, { fullResponse: true })
42 | console.log(res.rateLimit, res.data)
43 |
44 | // [headers]
45 | // Customize sent HTTP headers
46 | client.v1.post('statuses/update.json', { status: 'Hello' }, { headers: { 'X-Custom-Header': 'My Header Value' } })
47 | ```
--------------------------------------------------------------------------------
/doc/paginators.md:
--------------------------------------------------------------------------------
1 | # Paginators
2 |
3 | Most of the endpoints returning a paginable collection (user timeline, tweet search, home timeline, ...) returns a sub class of `TwitterPaginator`.
4 |
5 | By default, instance is built with the initial response data, and you have access to method to fetch automatically next page(s).
6 | ```ts
7 | const homeTimeline = await client.v1.homeTimeline();
8 | ```
9 |
10 | ## Iterate over the fetched items
11 |
12 | You can iterate with native `Symbol.iterator`.
13 |
14 | ```ts
15 | for (const fetchedTweet of homeTimeline) {
16 | // do something with fetchedTweet
17 | }
18 | ```
19 |
20 | ## Access the fetched items
21 |
22 | In paginators that contains
23 | - tweets: `.tweets`
24 | - lists: `.lists`
25 | - users: `.users`
26 | - Only IDs: `.ids`
27 | - Welcome messages: `.welcomeMessages`
28 | - Events (like DM events): `.events`
29 |
30 | ## Check if a next page is available
31 |
32 | You can know if a paginator is over by looking up the `.done` property.
33 |
34 | ```ts
35 | while (!homeTimeline.done) {
36 | await homeTimeline.fetchNext();
37 | }
38 | ```
39 |
40 | ## Download next page
41 |
42 | You can fetch the next page of the collection with the method `.fetchNext()`.
43 | ```ts
44 | await homeTimeline.fetchNext();
45 | ```
46 | Items will be added to current instance and next calls to `.tweets` or `Symbol.iterator` will include **previously fetched tweets and new tweets**.
47 |
48 | ## Get next page as a new instance
49 |
50 | Alternatively, you can use `.next()` to fetch a next page and get it as a separator instance.
51 | ```ts
52 | const timelinePage2 = await homeTimeline.next();
53 | ```
54 | Next page items will be only available on `timelinePage2.tweets` or through `timelinePage2[Symbol.iterator]`. New instance will **not** have items of previous page.
55 |
56 | ## Fetch until rate limit hits
57 |
58 | If you want to fetch the maximum of tweets or iterate though the whole possible collection, you can use async iterator or `.fetchLast()`.
59 |
60 | ```ts
61 | // Will fetch 1000 more tweets (automatically split into separate requests),
62 | // except if the rate limit is hit or if no more results are available
63 | await homeTimeline.fetchLast(1000);
64 | ```
65 |
66 | Alternatively, you can achieve this behaviour with async iterators:
67 | ```ts
68 | // Note presence of 'await' here
69 | for await (const tweet of homeTimeline) {
70 | // Iterate until rate limit is hit
71 | // or API calls returns no more results
72 | }
73 | ```
74 |
75 | ## v2 meta, includes
76 |
77 | For tweets endpoints that returns `meta`s and `includes` in their payload, `v2` paginators supports them (and merge them into a unique container :D),
78 | just use `Paginator.meta` or `Paginator.includes`.
79 |
80 | **`.includes` is an accessor to a `TwitterV2IncludesHelper` instance.** See how to [use it here](./helpers.md#helpers-for-includes-of-v2-api-responses).
81 |
82 | ```ts
83 | const mySearch = await client.v2.search('nodeJS');
84 |
85 | for await (const tweet of mySearch) {
86 | const availableMeta = mySearch.meta;
87 | const availableIncludes = mySearch.includes;
88 |
89 | // availableMeta and availableIncludes are filled with .meta and .includes
90 | // fetched at the time you were reading this tweet
91 | // Once the next page is automatically fetched, they can be updated!
92 | }
93 | ```
94 |
95 | ## v2 errors
96 |
97 | In some cases, paginators in v2 API can contains errors. You can access then with `.errors` getter.
98 |
99 | ## Previous page
100 |
101 | On paginators that supports it, you can get previous pages with `.previous()` and `.fetchPrevious()`.
102 |
103 | Their behaviour is comparable as `.next()` and `.fetchNext()`.
104 |
--------------------------------------------------------------------------------
/doc/plugins.md:
--------------------------------------------------------------------------------
1 | # Plugins for `twitter-api-v2`
2 |
3 | Since version `1.11.0`, library supports plugins.
4 | Plugins are objects exposing specific functions, called by the library at specific times.
5 |
6 | ## Using plugins
7 |
8 | Import your plugin, instantiate them (if needed), and give them in the `plugins` array of client settings.
9 |
10 | ```ts
11 | import { TwitterApi } from 'twitter-api-v2'
12 | import { TwitterApiCachePluginRedis } from '@twitter-api-v2/plugin-cache-redis'
13 |
14 | const redisPlugin = new TwitterApiCachePluginRedis(redisInstance)
15 |
16 | const client = new TwitterApi(yourKeys, { plugins: [redisPlugin] })
17 | ```
18 |
19 | ## Writing plugins
20 |
21 | You can write object/classes that implements the following interface:
22 | ```ts
23 | interface ITwitterApiClientPlugin {
24 | // Classic requests
25 | /* Executed when request is about to be prepared. OAuth headers, body, query normalization hasn't been done yet. */
26 | onBeforeRequestConfig?: TTwitterApiBeforeRequestConfigHook
27 | /* Executed when request is about to be made. Headers/body/query has been prepared, and HTTP options has been initialized. */
28 | onBeforeRequest?: TTwitterApiBeforeRequestHook
29 | /* Executed when a request succeeds (failed requests don't trigger this hook). */
30 | onAfterRequest?: TTwitterApiAfterRequestHook
31 | // Error handling in classic requests
32 | /* Executed when Twitter doesn't reply (network error, server disconnect). */
33 | onRequestError?: TTwitterApiRequestErrorHook
34 | /* Executed when Twitter reply but with an error? */
35 | onResponseError?: TTwitterApiResponseErrorHook
36 | // Stream requests
37 | /* Executed when a stream request is about to be prepared. This method **can't** return a `Promise`. */
38 | onBeforeStreamRequestConfig?: TTwitterApiBeforeStreamRequestConfigHook
39 | // Request token
40 | /* Executed after a `.generateAuthLink`, mainly to allow automatic collect of `oauth_token`/`oauth_token_secret` couples. */
41 | onOAuth1RequestToken?: TTwitterApiAfterOAuth1RequestTokenHook
42 | /* Executed after a `.generateOAuth2AuthLink`, mainly to allow automatic collect of `state`/`codeVerifier` couples. */
43 | onOAuth2RequestToken?: TTwitterApiAfterOAuth2RequestTokenHook
44 | }
45 | ```
46 |
47 | Every method is optional, because you can implement whatever you want to listen to.
48 |
49 | Method types:
50 | ```ts
51 | type TTwitterApiBeforeRequestConfigHook = (args: ITwitterApiBeforeRequestConfigHookArgs) => PromiseOrType | void>
52 | type TTwitterApiBeforeRequestHook = (args: ITwitterApiBeforeRequestHookArgs) => void | Promise
53 | type TTwitterApiAfterRequestHook = (args: ITwitterApiAfterRequestHookArgs) => PromiseOrType
54 | type TTwitterApiRequestErrorHook = (args: ITwitterApiRequestErrorHookArgs) => PromiseOrType
55 | type TTwitterApiResponseErrorHook = (args: ITwitterApiResponseErrorHookArgs) => PromiseOrType
56 | type TTwitterApiBeforeStreamRequestConfigHook = (args: ITwitterApiBeforeRequestConfigHookArgs) => void
57 | type TTwitterApiAfterOAuth1RequestTokenHook = (args: ITwitterApiAfterOAuth1RequestTokenHookArgs) => void | Promise
58 | type TTwitterApiAfterOAuth2RequestTokenHook = (args: ITwitterApiAfterOAuth2RequestTokenHookArgs) => void | Promise
59 | ```
60 |
61 | A simple plugin implementation that logs GET requests can be:
62 |
63 | ```ts
64 | class TwitterApiLoggerPlugin implements ITwitterApiClientPlugin {
65 | onBeforeRequestConfig(args: ITwitterApiBeforeRequestConfigHookArgs) {
66 | const method = args.params.method.toUpperCase()
67 | console.log(`${method} ${args.url.toString()} ${JSON.stringify(args.params.query)}`)
68 | }
69 | }
70 |
71 | const client = new TwitterApi(yourKeys, { plugins: [new TwitterApiLoggerPlugin()] })
72 | ```
73 |
--------------------------------------------------------------------------------
/doc/rate-limiting.md:
--------------------------------------------------------------------------------
1 | # Rate limiting
2 |
3 | ## Extract rate limit information with plugins
4 |
5 | Plugin `@twitter-api-v2/plugin-rate-limit` can help you to store/get rate limit information.
6 | It stores automatically rate limits sent by Twitter at each request and gives you an API to get them when you need to.
7 |
8 | ```ts
9 | import { TwitterApi } from 'twitter-api-v2'
10 | import { TwitterApiRateLimitPlugin } from '@twitter-api-v2/plugin-rate-limit'
11 |
12 | const rateLimitPlugin = new TwitterApiRateLimitPlugin()
13 | const client = new TwitterApi(yourKeys, { plugins: [rateLimitPlugin] })
14 |
15 | // ...make requests...
16 | await client.v2.me()
17 | // ...
18 |
19 | const currentRateLimitForMe = await rateLimitPlugin.v2.getRateLimit('users/me')
20 | console.log(currentRateLimitForMe.limit) // 75
21 | console.log(currentRateLimitForMe.remaining) // 74
22 | ```
23 |
24 | ## With HTTP methods helpers
25 |
26 | If you use a HTTP method helper (`.get`, `.post`, ...), you can get a **full response** object that directly contains the rate limit information,
27 | even if the request didn't fail!
28 | ```ts
29 | const manualFullResponse = await client.v1.get('statuses/home_timeline.json', { since_id: '20' }, { fullResponse: true });
30 |
31 | // Response data
32 | manualFullResponse.data; // TweetV1TimelineResult
33 | // Rate limit information
34 | manualFullResponse.rateLimit; // { limit: number, remaining: number, reset: number }
35 | ```
36 |
37 | ## Handle errors - Everywhere in this library
38 |
39 | This library helps you to handle rate limiting.
40 | When a request fails (with a Twitter response), it create a `ApiResponseError` instance and throw it.
41 |
42 | If `ApiResponseErrorInstance.rateLimitError` is `true`, then you just hit the rate limit.
43 | You have access to rate limit limits with `ApiResponseErrorInstance.rateLimit`:
44 |
45 | ```ts
46 | import { ApiResponseError } from 'twitter-api-v2';
47 |
48 | try {
49 | // Get a single tweet
50 | await client.v2.tweet('20');
51 | } catch (error) {
52 | if (error instanceof ApiResponseError && error.rateLimitError && error.rateLimit) {
53 | console.log(`You just hit the rate limit! Limit for this endpoint is ${error.rateLimit.limit} requests!`);
54 | console.log(`Request counter will reset at timestamp ${error.rateLimit.reset}.`);
55 | }
56 | }
57 | ```
58 |
59 | ---
60 |
61 | **Example**: You can automate this process with a waiter that will retry a failed request after the reset timer is over:
62 |
63 | *Warning*: This method can be VERY ineffective, as it can wait up to 15 minutes (Twitter's usual rate limit reset time).
64 | ```ts
65 | function sleep(ms: number) {
66 | return new Promise(resolve => setTimeout(resolve, ms));
67 | }
68 |
69 | async function autoRetryOnRateLimitError(callback: () => T | Promise) {
70 | while (true) {
71 | try {
72 | return await callback();
73 | } catch (error) {
74 | if (error instanceof ApiResponseError && error.rateLimitError && error.rateLimit) {
75 | const resetTimeout = error.rateLimit.reset * 1000; // convert to ms time instead of seconds time
76 | const timeToWait = resetTimeout - Date.now();
77 |
78 | await sleep(timeToWait);
79 | continue;
80 | }
81 |
82 | throw error;
83 | }
84 | }
85 | }
86 |
87 | // Then use it...
88 | await autoRetryOnRateLimitError(() => client.v2.tweet('20'));
89 | ```
90 |
91 | ## Special case of paginators
92 |
93 | Paginators will automatically handle the rate limit when using `.fetchLast` or async iteration.
94 | The `.fetchLast` or iteration will automatically ends when rate limit is hit.
95 |
96 | Moreover, you can access current rate limit status for paginator's endpoint with the `.rateLimit` getter.
97 | ```ts
98 | const paginator = await client.v1.homeTimeline();
99 | console.log(paginator.rateLimit); // { limit: number, remaining: number, reset: number }
100 | ```
101 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitter-api-v2",
3 | "version": "1.23.2",
4 | "description": "Strongly typed, full-featured, light, versatile yet powerful Twitter API v1.1 and v2 client for Node.js.",
5 | "main": "dist/cjs/index.js",
6 | "module": "dist/esm/index.js",
7 | "types": "dist/esm/index.d.ts",
8 | "keywords": [
9 | "twitter",
10 | "api",
11 | "typed",
12 | "types",
13 | "v2",
14 | "v1.1"
15 | ],
16 | "scripts": {
17 | "build": "npm run build:cjs && npm run build:esm",
18 | "build:cjs": "tsc -b tsconfig.cjs.json",
19 | "build:esm": "tsc -b tsconfig.esm.json",
20 | "build-doc": "typedoc src/index.ts --out tsdocs",
21 | "lint": "eslint --ext \".ts\" --ignore-path .gitignore .",
22 | "mocha": "mocha -r ts-node/register --timeout 10000",
23 | "test": "npm run mocha 'test/**/*.test.ts'",
24 | "test-tweet": "npm run mocha 'test/tweet.*.test.ts'",
25 | "test-user": "npm run mocha 'test/user.*.test.ts'",
26 | "test-stream": "npm run mocha test/stream.test.ts",
27 | "test-media": "npm run mocha test/media-upload.test.ts",
28 | "test-auth": "npm run mocha test/auth.test.ts",
29 | "test-dm": "npm run mocha test/dm.*.test.ts",
30 | "test-list": "npm run mocha test/list.*.test.ts",
31 | "test-space": "npm run mocha test/space.v2.test.ts",
32 | "test-account": "npm run mocha test/account.*.test.ts",
33 | "test-plugin": "npm run mocha test/plugin.test.ts",
34 | "prepublish": "npm run build"
35 | },
36 | "repository": "github:plhery/node-twitter-api-v2",
37 | "author": "Paul-Louis Hery (https://x.com/plhery)",
38 | "license": "Apache-2.0",
39 | "files": [
40 | "dist"
41 | ],
42 | "devDependencies": {
43 | "@types/chai": "^4.2.16",
44 | "@types/mocha": "^10.0.1",
45 | "@types/node": "^18.11.17",
46 | "@typescript-eslint/eslint-plugin": "^5.47.0",
47 | "@typescript-eslint/parser": "^5.47.0",
48 | "chai": "^4.3.4",
49 | "dotenv": "^16.0.3",
50 | "eslint": "^8.30.0",
51 | "mocha": "^10.0.0",
52 | "ts-node": "^10.9.1",
53 | "typedoc": "^0.23.23",
54 | "typescript": "^4.2.4"
55 | },
56 | "bugs": {
57 | "url": "https://github.com/plhery/node-twitter-api/issues"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/ads-sandbox/client.ads-sandbox.read.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiSubClient from '../client.subclient';
2 | import { API_ADS_SANDBOX_PREFIX } from '../globals';
3 |
4 | /**
5 | * Base Twitter ads sandbox client with only read rights.
6 | */
7 | export default class TwitterAdsSandboxReadOnly extends TwitterApiSubClient {
8 | protected _prefix = API_ADS_SANDBOX_PREFIX;
9 | }
10 |
--------------------------------------------------------------------------------
/src/ads-sandbox/client.ads-sandbox.ts:
--------------------------------------------------------------------------------
1 | import { API_ADS_SANDBOX_PREFIX } from '../globals';
2 | import TwitterAdsSandboxReadWrite from './client.ads-sandbox.write';
3 |
4 | /**
5 | * Twitter ads sandbox client with all rights (read/write)
6 | */
7 | export class TwitterAdsSandbox extends TwitterAdsSandboxReadWrite {
8 | protected _prefix = API_ADS_SANDBOX_PREFIX;
9 |
10 | /**
11 | * Get a client with read/write rights.
12 | */
13 | public get readWrite() {
14 | return this as TwitterAdsSandboxReadWrite;
15 | }
16 | }
17 |
18 | export default TwitterAdsSandbox;
19 |
--------------------------------------------------------------------------------
/src/ads-sandbox/client.ads-sandbox.write.ts:
--------------------------------------------------------------------------------
1 | import { API_ADS_SANDBOX_PREFIX } from '../globals';
2 | import TwitterAdsSandboxReadOnly from './client.ads-sandbox.read';
3 |
4 | /**
5 | * Base Twitter ads sandbox client with read/write rights.
6 | */
7 | export default class TwitterAdsSandboxReadWrite extends TwitterAdsSandboxReadOnly {
8 | protected _prefix = API_ADS_SANDBOX_PREFIX;
9 |
10 | /**
11 | * Get a client with only read rights.
12 | */
13 | public get readOnly() {
14 | return this as TwitterAdsSandboxReadOnly;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ads/client.ads.read.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiSubClient from '../client.subclient';
2 | import { API_ADS_PREFIX } from '../globals';
3 |
4 | /**
5 | * Base Twitter ads client with only read rights.
6 | */
7 | export default class TwitterAdsReadOnly extends TwitterApiSubClient {
8 | protected _prefix = API_ADS_PREFIX;
9 | }
10 |
--------------------------------------------------------------------------------
/src/ads/client.ads.ts:
--------------------------------------------------------------------------------
1 | import { API_ADS_PREFIX } from '../globals';
2 | import TwitterAdsReadWrite from './client.ads.write';
3 | import TwitterAdsSandbox from '../ads-sandbox/client.ads-sandbox';
4 |
5 | /**
6 | * Twitter ads client with all rights (read/write)
7 | */
8 | export class TwitterAds extends TwitterAdsReadWrite {
9 | protected _prefix = API_ADS_PREFIX;
10 | protected _sandbox?: TwitterAdsSandbox;
11 |
12 | /**
13 | * Get a client with read/write rights.
14 | */
15 | public get readWrite() {
16 | return this as TwitterAdsReadWrite;
17 | }
18 |
19 | /**
20 | * Get Twitter Ads Sandbox API client
21 | */
22 | public get sandbox() {
23 | if (this._sandbox) return this._sandbox;
24 | return this._sandbox = new TwitterAdsSandbox(this);
25 | }
26 | }
27 |
28 | export default TwitterAds;
29 |
--------------------------------------------------------------------------------
/src/ads/client.ads.write.ts:
--------------------------------------------------------------------------------
1 | import { API_ADS_PREFIX } from '../globals';
2 | import TwitterAdsReadOnly from './client.ads.read';
3 |
4 | /**
5 | * Base Twitter ads client with read/write rights.
6 | */
7 | export default class TwitterAdsReadWrite extends TwitterAdsReadOnly {
8 | protected _prefix = API_ADS_PREFIX;
9 |
10 | /**
11 | * Get a client with only read rights.
12 | */
13 | public get readOnly() {
14 | return this as TwitterAdsReadOnly;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/client-mixins/form-data.helper.ts:
--------------------------------------------------------------------------------
1 | import { arrayWrap } from '../helpers';
2 |
3 | type TStringable = { toString(): string; };
4 |
5 | // This class is partially inspired by https://github.com/form-data/form-data/blob/master/lib/form_data.js
6 | // All credits to their authors.
7 | export class FormDataHelper {
8 | protected _boundary: string = '';
9 | protected _chunks: Buffer[] = [];
10 | protected _footerChunk?: Buffer;
11 |
12 | protected static readonly LINE_BREAK = '\r\n';
13 | protected static readonly DEFAULT_CONTENT_TYPE = 'application/octet-stream';
14 |
15 | protected bodyAppend(...values: (Buffer | string)[]) {
16 | const allAsBuffer = values.map(val => val instanceof Buffer ? val : Buffer.from(val));
17 | this._chunks.push(...allAsBuffer);
18 | }
19 |
20 | append(field: string, value: Buffer | string | TStringable, contentType?: string) {
21 | const convertedValue = value instanceof Buffer ? value : value.toString();
22 |
23 | const header = this.getMultipartHeader(field, convertedValue, contentType);
24 |
25 | this.bodyAppend(header, convertedValue, FormDataHelper.LINE_BREAK);
26 | }
27 |
28 | getHeaders() {
29 | return {
30 | 'content-type': 'multipart/form-data; boundary=' + this.getBoundary(),
31 | };
32 | }
33 |
34 | /** Length of form-data (including footer length). */
35 | protected getLength() {
36 | return this._chunks.reduce(
37 | (acc, cur) => acc + cur.length,
38 | this.getMultipartFooter().length,
39 | );
40 | }
41 |
42 | getBuffer() {
43 | const allChunks = [...this._chunks, this.getMultipartFooter()];
44 | const totalBuffer = Buffer.alloc(this.getLength());
45 |
46 | let i = 0;
47 | for (const chunk of allChunks) {
48 | for (let j = 0; j < chunk.length; i++, j++) {
49 | totalBuffer[i] = chunk[j];
50 | }
51 | }
52 |
53 | return totalBuffer;
54 | }
55 |
56 | protected getBoundary() {
57 | if (!this._boundary) {
58 | this.generateBoundary();
59 | }
60 |
61 | return this._boundary;
62 | }
63 |
64 | protected generateBoundary() {
65 | // This generates a 50 character boundary similar to those used by Firefox.
66 | let boundary = '--------------------------';
67 | for (let i = 0; i < 24; i++) {
68 | boundary += Math.floor(Math.random() * 10).toString(16);
69 | }
70 |
71 | this._boundary = boundary;
72 | }
73 |
74 | protected getMultipartHeader(field: string, value: string | Buffer, contentType?: string) {
75 | // In this lib no need to guess more the content type, octet stream is ok of buffers
76 | if (!contentType) {
77 | contentType = value instanceof Buffer ? FormDataHelper.DEFAULT_CONTENT_TYPE : '';
78 | }
79 |
80 | const headers = {
81 | 'Content-Disposition': ['form-data', `name="${field}"`],
82 | 'Content-Type': contentType,
83 | };
84 |
85 | let contents = '';
86 | for (const [prop, header] of Object.entries(headers)) {
87 | // skip nullish headers.
88 | if (!header.length) {
89 | continue;
90 | }
91 |
92 | contents += prop + ': ' + arrayWrap(header).join('; ') + FormDataHelper.LINE_BREAK;
93 | }
94 |
95 | return '--' + this.getBoundary() + FormDataHelper.LINE_BREAK + contents + FormDataHelper.LINE_BREAK;
96 | }
97 |
98 | protected getMultipartFooter() {
99 | if (this._footerChunk) {
100 | return this._footerChunk;
101 | }
102 | return this._footerChunk = Buffer.from('--' + this.getBoundary() + '--' + FormDataHelper.LINE_BREAK);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/client-mixins/oauth1.helper.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'crypto';
2 |
3 | // ----------------------------------------------------------
4 | // LICENSE: This code partially belongs to oauth-1.0a package
5 | // ----------------------------------------------------------
6 |
7 | export interface OAuth1Tokens {
8 | key: string;
9 | secret: string;
10 | }
11 |
12 | export interface OAuth1MakerArgs {
13 | consumerKeys: OAuth1Tokens;
14 | }
15 |
16 | export interface OAuth1RequestOptions {
17 | url: string;
18 | method: string;
19 | data?: any;
20 | }
21 |
22 | export interface OAuth1AuthInfo {
23 | oauth_consumer_key: string;
24 | oauth_nonce: string;
25 | oauth_signature_method: string;
26 | oauth_timestamp: number;
27 | oauth_version: string;
28 | oauth_token: string;
29 | oauth_signature: string;
30 | }
31 |
32 | export class OAuth1Helper {
33 | nonceLength = 32;
34 | protected consumerKeys: OAuth1Tokens;
35 |
36 | constructor(options: OAuth1MakerArgs) {
37 | this.consumerKeys = options.consumerKeys;
38 | }
39 |
40 | static percentEncode(str: string) {
41 | return encodeURIComponent(str)
42 | .replace(/!/g, '%21')
43 | .replace(/\*/g, '%2A')
44 | .replace(/'/g, '%27')
45 | .replace(/\(/g, '%28')
46 | .replace(/\)/g, '%29');
47 | }
48 |
49 | protected hash(base: string, key: string) {
50 | return crypto
51 | .createHmac('sha1', key)
52 | .update(base)
53 | .digest('base64');
54 | }
55 |
56 | authorize(request: OAuth1RequestOptions, accessTokens: Partial = {}) {
57 | const oauthInfo: Partial = {
58 | oauth_consumer_key: this.consumerKeys.key,
59 | oauth_nonce: this.getNonce(),
60 | oauth_signature_method: 'HMAC-SHA1',
61 | oauth_timestamp: this.getTimestamp(),
62 | oauth_version: '1.0',
63 | };
64 |
65 | if (accessTokens.key !== undefined) {
66 | oauthInfo.oauth_token = accessTokens.key;
67 | }
68 |
69 | if (!request.data) {
70 | request.data = {};
71 | }
72 |
73 | oauthInfo.oauth_signature = this.getSignature(request, accessTokens.secret, oauthInfo as OAuth1AuthInfo);
74 |
75 | return oauthInfo as OAuth1AuthInfo;
76 | }
77 |
78 | toHeader(oauthInfo: OAuth1AuthInfo) {
79 | const sorted = sortObject(oauthInfo);
80 | let header_value = 'OAuth ';
81 |
82 | for (const element of sorted) {
83 | if (element.key.indexOf('oauth_') !== 0) {
84 | continue;
85 | }
86 |
87 | header_value += OAuth1Helper.percentEncode(element.key) + '="' + OAuth1Helper.percentEncode(element.value as string) + '",';
88 | }
89 |
90 | return {
91 | // Remove the last ,
92 | Authorization: header_value.slice(0, header_value.length - 1),
93 | };
94 | }
95 |
96 | protected getNonce() {
97 | const wordCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
98 | let result = '';
99 |
100 | for (let i = 0; i < this.nonceLength; i++) {
101 | result += wordCharacters[Math.trunc(Math.random() * wordCharacters.length)];
102 | }
103 |
104 | return result;
105 | }
106 |
107 | protected getTimestamp() {
108 | return Math.trunc(new Date().getTime() / 1000);
109 | }
110 |
111 | protected getSignature(request: OAuth1RequestOptions, tokenSecret: string | undefined, oauthInfo: OAuth1AuthInfo) {
112 | return this.hash(
113 | this.getBaseString(request, oauthInfo),
114 | this.getSigningKey(tokenSecret)
115 | );
116 | }
117 |
118 | protected getSigningKey(tokenSecret: string | undefined) {
119 | return OAuth1Helper.percentEncode(this.consumerKeys.secret) + '&' + OAuth1Helper.percentEncode(tokenSecret || '');
120 | }
121 |
122 | protected getBaseString(request: OAuth1RequestOptions, oauthInfo: OAuth1AuthInfo) {
123 | return request.method.toUpperCase() + '&'
124 | + OAuth1Helper.percentEncode(this.getBaseUrl(request.url)) + '&'
125 | + OAuth1Helper.percentEncode(this.getParameterString(request, oauthInfo));
126 | }
127 |
128 | protected getParameterString(request: OAuth1RequestOptions, oauthInfo: OAuth1AuthInfo) {
129 | const baseStringData = sortObject(
130 | percentEncodeData(
131 | mergeObject(
132 | oauthInfo,
133 | mergeObject(request.data, deParamUrl(request.url)),
134 | ),
135 | ),
136 | );
137 |
138 | let dataStr = '';
139 |
140 | for (const { key, value } of baseStringData) {
141 | // check if the value is an array
142 | // this means that this key has multiple values
143 | if (value && Array.isArray(value)) {
144 | // sort the array first
145 | value.sort();
146 |
147 | let valString = '';
148 | // serialize all values for this key: e.g. formkey=formvalue1&formkey=formvalue2
149 | value.forEach((item, i) => {
150 | valString += key + '=' + item;
151 | if (i < value.length){
152 | valString += '&';
153 | }
154 | });
155 |
156 | dataStr += valString;
157 | } else {
158 | dataStr += key + '=' + value + '&';
159 | }
160 | }
161 |
162 | // Remove the last character
163 | return dataStr.slice(0, dataStr.length - 1);
164 | }
165 |
166 | protected getBaseUrl(url: string) {
167 | return url.split('?')[0];
168 | }
169 | }
170 |
171 | export default OAuth1Helper;
172 |
173 | // Helper functions //
174 |
175 | function mergeObject(obj1: A, obj2: B): A & B {
176 | return {
177 | ...obj1 || {},
178 | ...obj2 || {},
179 | };
180 | }
181 |
182 | function sortObject(data: T) {
183 | return Object.keys(data)
184 | .sort()
185 | .map(key => ({ key, value: data[key as keyof typeof data] }));
186 | }
187 |
188 | function deParam(string: string) {
189 | const split = string.split('&');
190 | const data: { [key: string]: string | string[] } = {};
191 |
192 | for (const coupleKeyValue of split) {
193 | const [key, value = ''] = coupleKeyValue.split('=');
194 |
195 | // check if the key already exists
196 | // this can occur if the QS part of the url contains duplicate keys like this: ?formkey=formvalue1&formkey=formvalue2
197 | if (data[key]) {
198 | // the key exists already
199 | if (!Array.isArray(data[key])) {
200 | // replace the value with an array containing the already present value
201 | data[key] = [data[key] as string];
202 | }
203 | // and add the new found value to it
204 | (data[key] as string[]).push(decodeURIComponent(value));
205 | } else {
206 | // it doesn't exist, just put the found value in the data object
207 | data[key] = decodeURIComponent(value);
208 | }
209 | }
210 |
211 | return data;
212 | }
213 |
214 | function deParamUrl(url: string) {
215 | const tmp = url.split('?');
216 |
217 | if (tmp.length === 1)
218 | return {};
219 |
220 | return deParam(tmp[1]);
221 | }
222 |
223 | function percentEncodeData(data: T): T {
224 | const result: any = {};
225 |
226 | for (const key in data) {
227 | let value: any = data[key];
228 |
229 | // check if the value is an array
230 | if (value && Array.isArray(value)){
231 | value = value.map(v => OAuth1Helper.percentEncode(v));
232 | } else {
233 | value = OAuth1Helper.percentEncode(value);
234 | }
235 |
236 | result[OAuth1Helper.percentEncode(key)] = value;
237 | }
238 |
239 | return result;
240 | }
241 |
--------------------------------------------------------------------------------
/src/client-mixins/oauth2.helper.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'crypto';
2 |
3 | export class OAuth2Helper {
4 | static getCodeVerifier() {
5 | return this.generateRandomString(128);
6 | }
7 |
8 | static getCodeChallengeFromVerifier(verifier: string) {
9 | return this.escapeBase64Url(
10 | crypto
11 | .createHash('sha256')
12 | .update(verifier)
13 | .digest('base64'),
14 | );
15 | }
16 |
17 | static getAuthHeader(clientId: string, clientSecret: string) {
18 | const key = encodeURIComponent(clientId) + ':' + encodeURIComponent(clientSecret);
19 | return Buffer.from(key).toString('base64');
20 | }
21 |
22 | static generateRandomString(length: number) {
23 | let text = '';
24 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
25 | for (let i = 0; i < length; i++) {
26 | text += possible[Math.floor(Math.random() * possible.length)];
27 | }
28 | return text;
29 | }
30 |
31 | private static escapeBase64Url(string: string) {
32 | return string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/client-mixins/request-param.helper.ts:
--------------------------------------------------------------------------------
1 | import { FormDataHelper } from './form-data.helper';
2 | import type { RequestOptions } from 'https';
3 | import type { TBodyMode, TRequestBody, TRequestQuery, TRequestStringQuery } from '../types/request-maker.mixin.types';
4 | import OAuth1Helper from './oauth1.helper';
5 |
6 | /* Helpers functions that are specific to this class but do not depends on instance */
7 |
8 | export class RequestParamHelpers {
9 | static readonly JSON_1_1_ENDPOINTS = new Set([
10 | 'direct_messages/events/new.json',
11 | 'direct_messages/welcome_messages/new.json',
12 | 'direct_messages/welcome_messages/rules/new.json',
13 | 'media/metadata/create.json',
14 | 'collections/entries/curate.json',
15 | ]);
16 |
17 | static formatQueryToString(query: TRequestQuery) {
18 | const formattedQuery: TRequestStringQuery = {};
19 |
20 | for (const prop in query) {
21 | if (typeof query[prop] === 'string') {
22 | formattedQuery[prop] = query[prop] as string;
23 | }
24 | else if (typeof query[prop] !== 'undefined') {
25 | formattedQuery[prop] = String(query[prop]);
26 | }
27 | }
28 |
29 | return formattedQuery;
30 | }
31 |
32 | static autoDetectBodyType(url: URL) : TBodyMode {
33 | if (url.pathname.startsWith('/2/') || url.pathname.startsWith('/labs/2/')) {
34 | // oauth2 takes url encoded
35 | if (url.password.startsWith('/2/oauth2')) {
36 | return 'url';
37 | }
38 | // Twitter API v2 has JSON-encoded requests for everything else
39 | return 'json';
40 | }
41 |
42 | if (url.hostname === 'upload.x.com') {
43 | if (url.pathname === '/1.1/media/upload.json') {
44 | return 'form-data';
45 | }
46 | // json except for media/upload command, that is form-data.
47 | return 'json';
48 | }
49 |
50 | const endpoint = url.pathname.split('/1.1/', 2)[1];
51 |
52 | if (this.JSON_1_1_ENDPOINTS.has(endpoint)) {
53 | return 'json';
54 | }
55 | return 'url';
56 | }
57 |
58 | static addQueryParamsToUrl(url: URL, query: TRequestQuery) {
59 | const queryEntries = Object.entries(query) as [string, string][];
60 |
61 | if (queryEntries.length) {
62 | let search = '';
63 |
64 | for (const [key, value] of queryEntries) {
65 | search += (search.length ? '&' : '?') + `${OAuth1Helper.percentEncode(key)}=${OAuth1Helper.percentEncode(value)}`;
66 | }
67 |
68 | url.search = search;
69 | }
70 | }
71 |
72 | static constructBodyParams(
73 | body: TRequestBody,
74 | headers: Record,
75 | mode: TBodyMode,
76 | ) {
77 | if (body instanceof Buffer) {
78 | return body;
79 | }
80 |
81 | if (mode === 'json') {
82 | if (!headers['content-type']) {
83 | headers['content-type'] = 'application/json;charset=UTF-8';
84 | }
85 | return JSON.stringify(body);
86 | }
87 | else if (mode === 'url') {
88 | if (!headers['content-type']) {
89 | headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
90 | }
91 |
92 | if (Object.keys(body).length) {
93 | return new URLSearchParams(body)
94 | .toString()
95 | .replace(/\*/g, '%2A'); // URLSearchParams doesnt encode '*', but Twitter wants it encoded.
96 | }
97 |
98 | return '';
99 | }
100 | else if (mode === 'raw') {
101 | throw new Error('You can only use raw body mode with Buffers. To give a string, use Buffer.from(str).');
102 | }
103 | else {
104 | const form = new FormDataHelper();
105 |
106 | for (const parameter in body) {
107 | form.append(parameter, body[parameter]);
108 | }
109 |
110 | if (!headers['content-type']) {
111 | const formHeaders = form.getHeaders();
112 | headers['content-type'] = formHeaders['content-type'];
113 | }
114 |
115 | return form.getBuffer();
116 | }
117 | }
118 |
119 | static setBodyLengthHeader(options: RequestOptions, body: string | Buffer) {
120 | options.headers = options.headers ?? {};
121 |
122 | if (typeof body === 'string') {
123 | options.headers['content-length'] = Buffer.byteLength(body);
124 | }
125 | else {
126 | options.headers['content-length'] = body.length;
127 | }
128 | }
129 |
130 | static isOAuthSerializable(item: any) {
131 | return !(item instanceof Buffer);
132 | }
133 |
134 | static mergeQueryAndBodyForOAuth(query: TRequestQuery, body: TRequestBody) {
135 | const parameters: any = {};
136 |
137 | for (const prop in query) {
138 | parameters[prop] = query[prop];
139 | }
140 |
141 | if (this.isOAuthSerializable(body)) {
142 | for (const prop in body) {
143 | const bodyProp = (body as any)[prop];
144 |
145 | if (this.isOAuthSerializable(bodyProp)) {
146 | parameters[prop] = typeof bodyProp === 'object' && bodyProp !== null && 'toString' in bodyProp
147 | ? bodyProp.toString()
148 | : bodyProp;
149 | }
150 | }
151 | }
152 |
153 | return parameters;
154 | }
155 |
156 | static moveUrlQueryParamsIntoObject(url: URL, query: TRequestQuery) {
157 | for (const [param, value] of url.searchParams) {
158 | query[param] = value;
159 | }
160 |
161 | // Remove the query string
162 | url.search = '';
163 | return url;
164 | }
165 |
166 | /**
167 | * Replace URL parameters available in pathname, like `:id`, with data given in `parameters`:
168 | * `https://x.com/:id.json` + `{ id: '20' }` => `https://x.com/20.json`
169 | */
170 | static applyRequestParametersToUrl(url: URL, parameters: TRequestQuery) {
171 | url.pathname = url.pathname.replace(/:([A-Z_-]+)/ig, (fullMatch, paramName: string) => {
172 | if (parameters[paramName] !== undefined) {
173 | return String(parameters[paramName]);
174 | }
175 | return fullMatch;
176 | });
177 |
178 | return url;
179 | }
180 | }
181 |
182 | export default RequestParamHelpers;
183 |
--------------------------------------------------------------------------------
/src/client.subclient.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiBase from './client.base';
2 |
3 | /**
4 | * Base subclient for every v1 and v2 client.
5 | */
6 | export default abstract class TwitterApiSubClient extends TwitterApiBase {
7 | constructor(instance: TwitterApiBase) {
8 | if (!(instance instanceof TwitterApiBase)) {
9 | throw new Error('You must instance SubTwitterApi instance from existing TwitterApi instance.');
10 | }
11 |
12 | super(instance);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/client/index.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiv1 from '../v1/client.v1';
2 | import TwitterApiv2 from '../v2/client.v2';
3 | import { TwitterApiError } from '../types';
4 | import TwitterApiReadWrite from './readwrite';
5 | import TwitterAds from '../ads/client.ads';
6 |
7 |
8 | // "Real" exported client for usage of TwitterApi.
9 | /**
10 | * Twitter v1.1 and v2 API client.
11 | */
12 | export class TwitterApi extends TwitterApiReadWrite {
13 | protected _v1?: TwitterApiv1;
14 | protected _v2?: TwitterApiv2;
15 | protected _ads?: TwitterAds;
16 |
17 | /* Direct access to subclients */
18 | public get v1() {
19 | if (this._v1) return this._v1;
20 |
21 | return this._v1 = new TwitterApiv1(this);
22 | }
23 |
24 | public get v2() {
25 | if (this._v2) return this._v2;
26 |
27 | return this._v2 = new TwitterApiv2(this);
28 | }
29 |
30 | /**
31 | * Get a client with read/write rights.
32 | */
33 | public get readWrite() {
34 | return this as TwitterApiReadWrite;
35 | }
36 |
37 | /**
38 | * Get Twitter Ads API client
39 | */
40 | public get ads() {
41 | if (this._ads) return this._ads;
42 | return this._ads = new TwitterAds(this);
43 | }
44 |
45 | /* Static helpers */
46 | public static getErrors(error: any) {
47 | if (typeof error !== 'object')
48 | return [];
49 |
50 | if (!('data' in error))
51 | return [];
52 |
53 | return (error as TwitterApiError).data.errors ?? [];
54 | }
55 |
56 | /** Extract another image size than obtained in a `profile_image_url` or `profile_image_url_https` field of a user object. */
57 | public static getProfileImageInSize(profileImageUrl: string, size: 'normal' | 'bigger' | 'mini' | 'original') {
58 | const lastPart = profileImageUrl.split('/').pop()!;
59 | const sizes = ['normal', 'bigger', 'mini'];
60 |
61 | let originalUrl = profileImageUrl;
62 |
63 | for (const availableSize of sizes) {
64 | if (lastPart.includes(`_${availableSize}`)) {
65 | originalUrl = profileImageUrl.replace(`_${availableSize}`, '');
66 | break;
67 | }
68 | }
69 |
70 | if (size === 'original') {
71 | return originalUrl;
72 | }
73 |
74 | const extPos = originalUrl.lastIndexOf('.');
75 | if (extPos !== -1) {
76 | const ext = originalUrl.slice(extPos + 1);
77 | return originalUrl.slice(0, extPos) + '_' + size + '.' + ext;
78 | } else {
79 | return originalUrl + '_' + size;
80 | }
81 | }
82 | }
83 |
84 | export { default as TwitterApiReadWrite } from './readwrite';
85 | export { default as TwitterApiReadOnly } from './readonly';
86 | export default TwitterApi;
87 |
--------------------------------------------------------------------------------
/src/client/readwrite.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiv1ReadWrite from '../v1/client.v1.write';
2 | import TwitterApiv2ReadWrite from '../v2/client.v2.write';
3 | import TwitterApiReadOnly from './readonly';
4 |
5 | /**
6 | * Twitter v1.1 and v2 API client.
7 | */
8 | export default class TwitterApiReadWrite extends TwitterApiReadOnly {
9 | protected _v1?: TwitterApiv1ReadWrite;
10 | protected _v2?: TwitterApiv2ReadWrite;
11 |
12 | /* Direct access to subclients */
13 | public get v1() {
14 | if (this._v1) return this._v1;
15 |
16 | return this._v1 = new TwitterApiv1ReadWrite(this);
17 | }
18 |
19 | public get v2() {
20 | if (this._v2) return this._v2;
21 |
22 | return this._v2 = new TwitterApiv2ReadWrite(this);
23 | }
24 |
25 | /**
26 | * Get a client with read only rights.
27 | */
28 | public get readOnly() {
29 | return this as TwitterApiReadOnly;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/globals.ts:
--------------------------------------------------------------------------------
1 | export const API_V2_PREFIX = 'https://api.x.com/2/';
2 | export const API_V2_LABS_PREFIX = 'https://api.x.com/labs/2/';
3 | export const API_V1_1_PREFIX = 'https://api.x.com/1.1/';
4 | export const API_V1_1_UPLOAD_PREFIX = 'https://upload.x.com/1.1/';
5 | export const API_V1_1_STREAM_PREFIX = 'https://stream.x.com/1.1/';
6 | export const API_ADS_PREFIX = 'https://ads-api.x.com/12/';
7 | export const API_ADS_SANDBOX_PREFIX = 'https://ads-api-sandbox.twitter.com/12/';
8 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { TwitterApiV2Settings } from './settings';
2 |
3 | export interface SharedPromise {
4 | value: T | undefined;
5 | promise: Promise;
6 | }
7 |
8 | export function sharedPromise(getter: () => Promise) {
9 | const sharedPromise: SharedPromise = {
10 | value: undefined,
11 | promise: getter().then(val => {
12 | sharedPromise.value = val;
13 | return val;
14 | }),
15 | };
16 |
17 | return sharedPromise;
18 | }
19 |
20 | export function arrayWrap(value: T | T[]) : T[] {
21 | if (Array.isArray(value)) {
22 | return value;
23 | }
24 | return [value];
25 | }
26 |
27 | export function trimUndefinedProperties(object: any) {
28 | // Delete undefined parameters
29 | for (const parameter in object) {
30 | if (object[parameter] === undefined)
31 | delete object[parameter];
32 | }
33 | }
34 |
35 | export function isTweetStreamV2ErrorPayload(payload: any) {
36 | // Is error only if 'errors' is present and 'data' does not exists
37 | return typeof payload === 'object'
38 | && 'errors' in payload
39 | && !('data' in payload);
40 | }
41 |
42 | export function hasMultipleItems(item: string | string[]) {
43 | if (Array.isArray(item) && item.length > 1) {
44 | return true;
45 | }
46 | return item.toString().includes(',');
47 | }
48 |
49 | /* Deprecation warnings */
50 |
51 | export interface IDeprecationWarning {
52 | instance: string;
53 | method: string;
54 | problem: string;
55 | resolution: string;
56 | }
57 |
58 | const deprecationWarningsCache = new Set();
59 |
60 | export function safeDeprecationWarning(message: IDeprecationWarning) {
61 | if (typeof console === 'undefined' || !console.warn || !TwitterApiV2Settings.deprecationWarnings) {
62 | return;
63 | }
64 |
65 | const hash = `${message.instance}-${message.method}-${message.problem}`;
66 | if (deprecationWarningsCache.has(hash)) {
67 | return;
68 | }
69 |
70 | const formattedMsg = `[twitter-api-v2] Deprecation warning: In ${message.instance}.${message.method}() call` +
71 | `, ${message.problem}.\n${message.resolution}.`;
72 |
73 | console.warn(formattedMsg);
74 | console.warn('To disable this message, import variable TwitterApiV2Settings from twitter-api-v2 and set TwitterApiV2Settings.deprecationWarnings to false.');
75 | deprecationWarningsCache.add(hash);
76 | }
77 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 |
2 | export { default as default } from './client';
3 | export * from './client';
4 | export * from './v1/client.v1';
5 | export * from './v2/client.v2';
6 | export * from './v2/includes.v2.helper';
7 | export * from './v2-labs/client.v2.labs';
8 | export * from './types';
9 | export * from './paginators';
10 | export * from './stream/TweetStream';
11 | export * from './settings';
12 |
--------------------------------------------------------------------------------
/src/paginators/dm.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import type { GetDmListV1Args, ReceivedDMEventsV1, TReceivedDMEvent, TwitterResponse, ReceivedWelcomeDMCreateEventV1, WelcomeDirectMessageListV1Result } from '../types';
2 | import { CursoredV1Paginator } from './paginator.v1';
3 |
4 | export class DmEventsV1Paginator extends CursoredV1Paginator {
5 | protected _endpoint = 'direct_messages/events/list.json';
6 |
7 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
8 | const result = response.data;
9 | this._rateLimit = response.rateLimit!;
10 |
11 | if (isNextPage) {
12 | this._realData.events.push(...result.events);
13 | this._realData.next_cursor = result.next_cursor;
14 | }
15 | }
16 |
17 | protected getPageLengthFromRequest(result: TwitterResponse) {
18 | return result.data.events.length;
19 | }
20 |
21 | protected getItemArray() {
22 | return this.events;
23 | }
24 |
25 | /**
26 | * Events returned by paginator.
27 | */
28 | get events() {
29 | return this._realData.events;
30 | }
31 | }
32 |
33 | export class WelcomeDmV1Paginator extends CursoredV1Paginator {
34 | protected _endpoint = 'direct_messages/welcome_messages/list.json';
35 |
36 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
37 | const result = response.data;
38 | this._rateLimit = response.rateLimit!;
39 |
40 | if (isNextPage) {
41 | this._realData.welcome_messages.push(...result.welcome_messages);
42 | this._realData.next_cursor = result.next_cursor;
43 | }
44 | }
45 |
46 | protected getPageLengthFromRequest(result: TwitterResponse) {
47 | return result.data.welcome_messages.length;
48 | }
49 |
50 | protected getItemArray() {
51 | return this.welcomeMessages;
52 | }
53 |
54 | get welcomeMessages() {
55 | return this._realData.welcome_messages;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/paginators/dm.paginator.v2.ts:
--------------------------------------------------------------------------------
1 | import { TimelineV2Paginator } from './v2.paginator';
2 | import { DMEventV2, GetDMEventV2Params, GetDMEventV2Result } from '../types/v2/dm.v2.types';
3 |
4 | export abstract class DMTimelineV2Paginator extends TimelineV2Paginator {
5 | protected getItemArray() {
6 | return this.events;
7 | }
8 |
9 | /**
10 | * Events returned by paginator.
11 | */
12 | get events() {
13 | return this._realData.data ?? [];
14 | }
15 |
16 | get meta() {
17 | return super.meta as GetDMEventV2Result['meta'];
18 | }
19 | }
20 |
21 | export class FullDMTimelineV2Paginator extends DMTimelineV2Paginator {
22 | protected _endpoint = 'dm_events';
23 | }
24 |
25 | export class OneToOneDMTimelineV2Paginator extends DMTimelineV2Paginator<{ participant_id: string }> {
26 | protected _endpoint = 'dm_conversations/with/:participant_id/dm_events';
27 | }
28 |
29 | export class ConversationDMTimelineV2Paginator extends DMTimelineV2Paginator<{ dm_conversation_id: string }> {
30 | protected _endpoint = 'dm_conversations/:dm_conversation_id/dm_events';
31 | }
32 |
--------------------------------------------------------------------------------
/src/paginators/followers.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import { CursoredV1Paginator } from './paginator.v1';
2 | import type { UserFollowerIdsV1Params, UserFollowerIdsV1Result, UserFollowerListV1Params, UserFollowerListV1Result, TwitterResponse, UserV1 } from '../types';
3 |
4 | export class UserFollowerListV1Paginator extends CursoredV1Paginator {
5 | protected _endpoint = 'followers/list.json';
6 |
7 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
8 | const result = response.data;
9 | this._rateLimit = response.rateLimit!;
10 |
11 | if (isNextPage) {
12 | this._realData.users.push(...result.users);
13 | this._realData.next_cursor = result.next_cursor;
14 | }
15 | }
16 |
17 | protected getPageLengthFromRequest(result: TwitterResponse) {
18 | return result.data.users.length;
19 | }
20 |
21 | protected getItemArray() {
22 | return this.users;
23 | }
24 |
25 | /**
26 | * Users returned by paginator.
27 | */
28 | get users() {
29 | return this._realData.users;
30 | }
31 | }
32 |
33 | export class UserFollowerIdsV1Paginator extends CursoredV1Paginator {
34 | protected _endpoint = 'followers/ids.json';
35 | protected _maxResultsWhenFetchLast = 5000;
36 |
37 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
38 | const result = response.data;
39 | this._rateLimit = response.rateLimit!;
40 |
41 | if (isNextPage) {
42 | this._realData.ids.push(...result.ids);
43 | this._realData.next_cursor = result.next_cursor;
44 | }
45 | }
46 |
47 | protected getPageLengthFromRequest(result: TwitterResponse) {
48 | return result.data.ids.length;
49 | }
50 |
51 | protected getItemArray() {
52 | return this.ids;
53 | }
54 |
55 | /**
56 | * Users IDs returned by paginator.
57 | */
58 | get ids() {
59 | return this._realData.ids;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/paginators/friends.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import { CursoredV1Paginator } from './paginator.v1';
2 | import type { UserFollowerIdsV1Params, UserFollowerIdsV1Result, UserFriendListV1Params, UserFriendListV1Result, UserV1, TwitterResponse } from '../types';
3 |
4 | export class UserFriendListV1Paginator extends CursoredV1Paginator {
5 | protected _endpoint = 'friends/list.json';
6 |
7 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
8 | const result = response.data;
9 | this._rateLimit = response.rateLimit!;
10 |
11 | if (isNextPage) {
12 | this._realData.users.push(...result.users);
13 | this._realData.next_cursor = result.next_cursor;
14 | }
15 | }
16 |
17 | protected getPageLengthFromRequest(result: TwitterResponse) {
18 | return result.data.users.length;
19 | }
20 |
21 | protected getItemArray() {
22 | return this.users;
23 | }
24 |
25 | /**
26 | * Users returned by paginator.
27 | */
28 | get users() {
29 | return this._realData.users;
30 | }
31 | }
32 |
33 | export class UserFollowersIdsV1Paginator extends CursoredV1Paginator {
34 | protected _endpoint = 'friends/ids.json';
35 | protected _maxResultsWhenFetchLast = 5000;
36 |
37 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
38 | const result = response.data;
39 | this._rateLimit = response.rateLimit!;
40 |
41 | if (isNextPage) {
42 | this._realData.ids.push(...result.ids);
43 | this._realData.next_cursor = result.next_cursor;
44 | }
45 | }
46 |
47 | protected getPageLengthFromRequest(result: TwitterResponse) {
48 | return result.data.ids.length;
49 | }
50 |
51 | protected getItemArray() {
52 | return this.ids;
53 | }
54 |
55 | /**
56 | * Users IDs returned by paginator.
57 | */
58 | get ids() {
59 | return this._realData.ids;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/paginators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tweet.paginator.v2';
2 | export * from './TwitterPaginator';
3 | export * from './dm.paginator.v1';
4 | export * from './mutes.paginator.v1';
5 | export * from './tweet.paginator.v1';
6 | export * from './user.paginator.v1';
7 | export * from './user.paginator.v2';
8 | export * from './list.paginator.v1';
9 | export * from './list.paginator.v2';
10 | export * from './friends.paginator.v1';
11 | export * from './followers.paginator.v1';
12 |
--------------------------------------------------------------------------------
/src/paginators/list.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import { DoubleEndedListsCursorV1Result, DoubleEndedUsersCursorV1Result, ListMembersV1Params, ListOwnershipsV1Params, ListV1, TwitterResponse, UserV1 } from '../types';
2 | import { CursoredV1Paginator } from './paginator.v1';
3 |
4 | abstract class ListListsV1Paginator extends CursoredV1Paginator {
5 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
6 | const result = response.data;
7 | this._rateLimit = response.rateLimit!;
8 |
9 | if (isNextPage) {
10 | this._realData.lists.push(...result.lists);
11 | this._realData.next_cursor = result.next_cursor;
12 | }
13 | }
14 |
15 | protected getPageLengthFromRequest(result: TwitterResponse) {
16 | return result.data.lists.length;
17 | }
18 |
19 | protected getItemArray() {
20 | return this.lists;
21 | }
22 |
23 | /**
24 | * Lists returned by paginator.
25 | */
26 | get lists() {
27 | return this._realData.lists;
28 | }
29 | }
30 |
31 | export class ListMembershipsV1Paginator extends ListListsV1Paginator {
32 | protected _endpoint = 'lists/memberships.json';
33 | }
34 |
35 | export class ListOwnershipsV1Paginator extends ListListsV1Paginator {
36 | protected _endpoint = 'lists/ownerships.json';
37 | }
38 |
39 | export class ListSubscriptionsV1Paginator extends ListListsV1Paginator {
40 | protected _endpoint = 'lists/subscriptions.json';
41 | }
42 |
43 | abstract class ListUsersV1Paginator extends CursoredV1Paginator {
44 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
45 | const result = response.data;
46 | this._rateLimit = response.rateLimit!;
47 |
48 | if (isNextPage) {
49 | this._realData.users.push(...result.users);
50 | this._realData.next_cursor = result.next_cursor;
51 | }
52 | }
53 |
54 | protected getPageLengthFromRequest(result: TwitterResponse) {
55 | return result.data.users.length;
56 | }
57 |
58 | protected getItemArray() {
59 | return this.users;
60 | }
61 |
62 | /**
63 | * Users returned by paginator.
64 | */
65 | get users() {
66 | return this._realData.users;
67 | }
68 | }
69 |
70 | export class ListMembersV1Paginator extends ListUsersV1Paginator {
71 | protected _endpoint = 'lists/members.json';
72 | }
73 |
74 | export class ListSubscribersV1Paginator extends ListUsersV1Paginator {
75 | protected _endpoint = 'lists/subscribers.json';
76 | }
77 |
--------------------------------------------------------------------------------
/src/paginators/list.paginator.v2.ts:
--------------------------------------------------------------------------------
1 | import type { GetListTimelineV2Params, ListTimelineV2Result, ListV2 } from '../types';
2 | import { TimelineV2Paginator } from './v2.paginator';
3 |
4 | abstract class ListTimelineV2Paginator<
5 | TResult extends ListTimelineV2Result,
6 | TParams extends GetListTimelineV2Params,
7 | TShared = any,
8 | > extends TimelineV2Paginator {
9 | protected getItemArray() {
10 | return this.lists;
11 | }
12 |
13 | /**
14 | * Lists returned by paginator.
15 | */
16 | get lists() {
17 | return this._realData.data ?? [];
18 | }
19 |
20 | get meta() {
21 | return super.meta as TResult['meta'];
22 | }
23 | }
24 |
25 | export class UserOwnedListsV2Paginator extends ListTimelineV2Paginator {
26 | protected _endpoint = 'users/:id/owned_lists';
27 | }
28 |
29 | export class UserListMembershipsV2Paginator extends ListTimelineV2Paginator {
30 | protected _endpoint = 'users/:id/list_memberships';
31 | }
32 |
33 | export class UserListFollowedV2Paginator extends ListTimelineV2Paginator {
34 | protected _endpoint = 'users/:id/followed_lists';
35 | }
36 |
--------------------------------------------------------------------------------
/src/paginators/mutes.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import { CursoredV1Paginator } from './paginator.v1';
2 | import type { MuteUserIdsV1Params, MuteUserIdsV1Result, MuteUserListV1Params, MuteUserListV1Result, TwitterResponse, UserV1 } from '../types';
3 |
4 | export class MuteUserListV1Paginator extends CursoredV1Paginator {
5 | protected _endpoint = 'mutes/users/list.json';
6 |
7 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
8 | const result = response.data;
9 | this._rateLimit = response.rateLimit!;
10 |
11 | if (isNextPage) {
12 | this._realData.users.push(...result.users);
13 | this._realData.next_cursor = result.next_cursor;
14 | }
15 | }
16 |
17 | protected getPageLengthFromRequest(result: TwitterResponse) {
18 | return result.data.users.length;
19 | }
20 |
21 | protected getItemArray() {
22 | return this.users;
23 | }
24 |
25 | /**
26 | * Users returned by paginator.
27 | */
28 | get users() {
29 | return this._realData.users;
30 | }
31 | }
32 |
33 | export class MuteUserIdsV1Paginator extends CursoredV1Paginator {
34 | protected _endpoint = 'mutes/users/ids.json';
35 | protected _maxResultsWhenFetchLast = 5000;
36 |
37 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
38 | const result = response.data;
39 | this._rateLimit = response.rateLimit!;
40 |
41 | if (isNextPage) {
42 | this._realData.ids.push(...result.ids);
43 | this._realData.next_cursor = result.next_cursor;
44 | }
45 | }
46 |
47 | protected getPageLengthFromRequest(result: TwitterResponse) {
48 | return result.data.ids.length;
49 | }
50 |
51 | protected getItemArray() {
52 | return this.ids;
53 | }
54 |
55 | /**
56 | * Users IDs returned by paginator.
57 | */
58 | get ids() {
59 | return this._realData.ids;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/paginators/paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import { TwitterResponse } from '../types';
2 | import TwitterPaginator from './TwitterPaginator';
3 |
4 | export abstract class CursoredV1Paginator<
5 | TApiResult extends { next_cursor_str?: string, next_cursor?: string },
6 | TApiParams extends { cursor?: string },
7 | TItem,
8 | TParams = any,
9 | > extends TwitterPaginator {
10 | protected getNextQueryParams(maxResults?: number): Partial {
11 | return {
12 | ...this._queryParams,
13 | cursor: this._realData.next_cursor_str ?? this._realData.next_cursor,
14 | ...(maxResults ? { count: maxResults } : {}),
15 | };
16 | }
17 |
18 | protected isFetchLastOver(result: TwitterResponse) {
19 | // If we cant fetch next page
20 | return !this.canFetchNextPage(result.data);
21 | }
22 |
23 | protected canFetchNextPage(result: TApiResult) {
24 | // If one of cursor is valid
25 | return !this.isNextCursorInvalid(result.next_cursor) || !this.isNextCursorInvalid(result.next_cursor_str);
26 | }
27 |
28 | private isNextCursorInvalid(value: string | number | undefined) {
29 | return value === undefined
30 | || value === 0
31 | || value === -1
32 | || value === '0'
33 | || value === '-1';
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/paginators/tweet.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import TwitterPaginator from './TwitterPaginator';
2 | import {
3 | TwitterResponse,
4 | TweetV1,
5 | TweetV1TimelineResult,
6 | TweetV1TimelineParams,
7 | TweetV1UserTimelineParams,
8 | ListStatusesV1Params,
9 | } from '../types';
10 |
11 | /** A generic TwitterPaginator able to consume TweetV1 timelines. */
12 | abstract class TweetTimelineV1Paginator<
13 | TResult extends TweetV1TimelineResult,
14 | TParams extends TweetV1TimelineParams,
15 | TShared = any,
16 | > extends TwitterPaginator {
17 | protected hasFinishedFetch = false;
18 |
19 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
20 | const result = response.data;
21 | this._rateLimit = response.rateLimit!;
22 |
23 | if (isNextPage) {
24 | this._realData.push(...result);
25 | // HINT: This is an approximation, as "end" of pagination cannot be safely determined without cursors.
26 | this.hasFinishedFetch = result.length === 0;
27 | }
28 | }
29 |
30 | protected getNextQueryParams(maxResults?: number) {
31 | const latestId = BigInt(this._realData[this._realData.length - 1].id_str);
32 |
33 | return {
34 | ...this.injectQueryParams(maxResults),
35 | max_id: (latestId - BigInt(1)).toString(),
36 | };
37 | }
38 |
39 | protected getPageLengthFromRequest(result: TwitterResponse) {
40 | return result.data.length;
41 | }
42 |
43 | protected isFetchLastOver(result: TwitterResponse) {
44 | return !result.data.length;
45 | }
46 |
47 | protected canFetchNextPage(result: TResult) {
48 | return result.length > 0;
49 | }
50 |
51 | protected getItemArray() {
52 | return this.tweets;
53 | }
54 |
55 | /**
56 | * Tweets returned by paginator.
57 | */
58 | get tweets() {
59 | return this._realData;
60 | }
61 |
62 | get done() {
63 | return super.done || this.hasFinishedFetch;
64 | }
65 | }
66 |
67 | // Timelines
68 |
69 | // Home
70 | export class HomeTimelineV1Paginator extends TweetTimelineV1Paginator {
71 | protected _endpoint = 'statuses/home_timeline.json';
72 | }
73 |
74 | // Mention
75 | export class MentionTimelineV1Paginator extends TweetTimelineV1Paginator {
76 | protected _endpoint = 'statuses/mentions_timeline.json';
77 | }
78 |
79 | // User
80 | export class UserTimelineV1Paginator extends TweetTimelineV1Paginator {
81 | protected _endpoint = 'statuses/user_timeline.json';
82 | }
83 |
84 | // Lists
85 | export class ListTimelineV1Paginator extends TweetTimelineV1Paginator {
86 | protected _endpoint = 'lists/statuses.json';
87 | }
88 |
89 | // Favorites
90 | export class UserFavoritesV1Paginator extends TweetTimelineV1Paginator {
91 | protected _endpoint = 'favorites/list.json';
92 | }
93 |
--------------------------------------------------------------------------------
/src/paginators/user.paginator.v1.ts:
--------------------------------------------------------------------------------
1 | import TwitterPaginator from './TwitterPaginator';
2 | import {
3 | FriendshipsIncomingV1Params,
4 | FriendshipsIncomingV1Result,
5 | TwitterResponse,
6 | UserSearchV1Params,
7 | UserV1,
8 | } from '../types';
9 | import { CursoredV1Paginator } from './paginator.v1';
10 |
11 | /** A generic TwitterPaginator able to consume TweetV1 timelines. */
12 | export class UserSearchV1Paginator extends TwitterPaginator {
13 | _endpoint = 'users/search.json';
14 |
15 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
16 | const result = response.data;
17 | this._rateLimit = response.rateLimit!;
18 |
19 | if (isNextPage) {
20 | this._realData.push(...result);
21 | }
22 | }
23 |
24 | protected getNextQueryParams(maxResults?: number) {
25 | const previousPage = Number(this._queryParams.page ?? '1');
26 |
27 | return {
28 | ...this._queryParams,
29 | page: previousPage + 1,
30 | ...maxResults ? { count: maxResults } : {},
31 | };
32 | }
33 |
34 | protected getPageLengthFromRequest(result: TwitterResponse) {
35 | return result.data.length;
36 | }
37 |
38 | protected isFetchLastOver(result: TwitterResponse) {
39 | return !result.data.length;
40 | }
41 |
42 | protected canFetchNextPage(result: UserV1[]) {
43 | return result.length > 0;
44 | }
45 |
46 | protected getItemArray() {
47 | return this.users;
48 | }
49 |
50 | /**
51 | * Users returned by paginator.
52 | */
53 | get users() {
54 | return this._realData;
55 | }
56 | }
57 |
58 | export class FriendshipsIncomingV1Paginator extends CursoredV1Paginator {
59 | protected _endpoint = 'friendships/incoming.json';
60 | protected _maxResultsWhenFetchLast = 5000;
61 |
62 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: true) {
63 | const result = response.data;
64 | this._rateLimit = response.rateLimit!;
65 |
66 | if (isNextPage) {
67 | this._realData.ids.push(...result.ids);
68 | this._realData.next_cursor = result.next_cursor;
69 | }
70 | }
71 |
72 | protected getPageLengthFromRequest(result: TwitterResponse) {
73 | return result.data.ids.length;
74 | }
75 |
76 | protected getItemArray() {
77 | return this.ids;
78 | }
79 |
80 | /**
81 | * Users IDs returned by paginator.
82 | */
83 | get ids() {
84 | return this._realData.ids;
85 | }
86 | }
87 |
88 | export class FriendshipsOutgoingV1Paginator extends FriendshipsIncomingV1Paginator {
89 | protected _endpoint = 'friendships/outgoing.json';
90 | }
91 |
--------------------------------------------------------------------------------
/src/paginators/user.paginator.v2.ts:
--------------------------------------------------------------------------------
1 | import { UserV2, UserV2TimelineParams, UserV2TimelineResult } from '../types';
2 | import { TimelineV2Paginator } from './v2.paginator';
3 |
4 | /** A generic PreviousableTwitterPaginator able to consume UserV2 timelines. */
5 | abstract class UserTimelineV2Paginator<
6 | TResult extends UserV2TimelineResult,
7 | TParams extends UserV2TimelineParams,
8 | TShared = any,
9 | > extends TimelineV2Paginator {
10 | protected getItemArray() {
11 | return this.users;
12 | }
13 |
14 | /**
15 | * Users returned by paginator.
16 | */
17 | get users() {
18 | return this._realData.data ?? [];
19 | }
20 |
21 | get meta() {
22 | return super.meta as TResult['meta'];
23 | }
24 | }
25 |
26 | export class UserBlockingUsersV2Paginator extends UserTimelineV2Paginator {
27 | protected _endpoint = 'users/:id/blocking';
28 | }
29 |
30 | export class UserMutingUsersV2Paginator extends UserTimelineV2Paginator {
31 | protected _endpoint = 'users/:id/muting';
32 | }
33 |
34 | export class UserFollowersV2Paginator extends UserTimelineV2Paginator {
35 | protected _endpoint = 'users/:id/followers';
36 | }
37 |
38 | export class UserFollowingV2Paginator extends UserTimelineV2Paginator {
39 | protected _endpoint = 'users/:id/following';
40 | }
41 |
42 | export class UserListMembersV2Paginator extends UserTimelineV2Paginator {
43 | protected _endpoint = 'lists/:id/members';
44 | }
45 |
46 | export class UserListFollowersV2Paginator extends UserTimelineV2Paginator {
47 | protected _endpoint = 'lists/:id/followers';
48 | }
49 |
50 | export class TweetLikingUsersV2Paginator extends UserTimelineV2Paginator {
51 | protected _endpoint = 'tweets/:id/liking_users';
52 | }
53 |
54 | export class TweetRetweetersUsersV2Paginator extends UserTimelineV2Paginator {
55 | protected _endpoint = 'tweets/:id/retweeted_by';
56 | }
57 |
--------------------------------------------------------------------------------
/src/paginators/v2.paginator.ts:
--------------------------------------------------------------------------------
1 | import type { TwitterResponse } from '../types';
2 | import type { DataMetaAndIncludeV2 } from '../types/v2/shared.v2.types';
3 | import { TwitterV2IncludesHelper } from '../v2/includes.v2.helper';
4 | import { PreviousableTwitterPaginator } from './TwitterPaginator';
5 |
6 | /** A generic PreviousableTwitterPaginator with common v2 helper methods. */
7 | export abstract class TwitterV2Paginator<
8 | TResult extends DataMetaAndIncludeV2,
9 | TParams extends object,
10 | TItem,
11 | TShared = any,
12 | > extends PreviousableTwitterPaginator {
13 | protected _includesInstance?: TwitterV2IncludesHelper;
14 |
15 | protected updateIncludes(data: TResult) {
16 | // Update errors
17 | if (data.errors) {
18 | if (!this._realData.errors) {
19 | this._realData.errors = [];
20 | }
21 | this._realData.errors = [...this._realData.errors, ...data.errors];
22 | }
23 |
24 | // Update includes
25 | if (!data.includes) {
26 | return;
27 | }
28 | if (!this._realData.includes) {
29 | this._realData.includes = {};
30 | }
31 |
32 | const includesRealData = this._realData.includes;
33 |
34 | for (const [includeKey, includeArray] of Object.entries(data.includes) as [keyof any, any[]][]) {
35 | if (!includesRealData[includeKey]) {
36 | includesRealData[includeKey] = [];
37 | }
38 |
39 | includesRealData[includeKey] = [
40 | ...includesRealData[includeKey]!,
41 | ...includeArray,
42 | ];
43 | }
44 | }
45 |
46 | /** Throw if the current paginator is not usable. */
47 | protected assertUsable() {
48 | if (this.unusable) {
49 | throw new Error(
50 | 'Unable to use this paginator to fetch more data, as it does not contain any metadata.' +
51 | ' Check .errors property for more details.',
52 | );
53 | }
54 | }
55 |
56 | get meta() {
57 | return this._realData.meta;
58 | }
59 |
60 | get includes() {
61 | if (!this._realData?.includes) {
62 | return new TwitterV2IncludesHelper(this._realData);
63 | }
64 | if (this._includesInstance) {
65 | return this._includesInstance;
66 | }
67 | return this._includesInstance = new TwitterV2IncludesHelper(this._realData);
68 | }
69 |
70 | get errors() {
71 | return this._realData.errors ?? [];
72 | }
73 |
74 | /** `true` if this paginator only contains error payload and no metadata found to consume data. */
75 | get unusable() {
76 | return this.errors.length > 0 && !this._realData.meta && !this._realData.data;
77 | }
78 | }
79 |
80 | /** A generic TwitterV2Paginator able to consume v2 timelines that use max_results and pagination tokens. */
81 | export abstract class TimelineV2Paginator<
82 | TResult extends DataMetaAndIncludeV2,
83 | TParams extends { max_results?: number, pagination_token?: string },
84 | TItem,
85 | TShared = any,
86 | > extends TwitterV2Paginator {
87 | protected refreshInstanceFromResult(response: TwitterResponse, isNextPage: boolean) {
88 | const result = response.data;
89 | const resultData = result.data ?? [];
90 | this._rateLimit = response.rateLimit!;
91 |
92 | if (!this._realData.data) {
93 | this._realData.data = [];
94 | }
95 |
96 | if (isNextPage) {
97 | this._realData.meta.result_count += result.meta.result_count;
98 | this._realData.meta.next_token = result.meta.next_token;
99 | this._realData.data.push(...resultData);
100 | }
101 | else {
102 | this._realData.meta.result_count += result.meta.result_count;
103 | this._realData.meta.previous_token = result.meta.previous_token;
104 | this._realData.data.unshift(...resultData);
105 | }
106 |
107 | this.updateIncludes(result);
108 | }
109 |
110 | protected getNextQueryParams(maxResults?: number) {
111 | this.assertUsable();
112 |
113 | return {
114 | ...this.injectQueryParams(maxResults),
115 | pagination_token: this._realData.meta.next_token,
116 | };
117 | }
118 |
119 | protected getPreviousQueryParams(maxResults?: number) {
120 | this.assertUsable();
121 |
122 | return {
123 | ...this.injectQueryParams(maxResults),
124 | pagination_token: this._realData.meta.previous_token,
125 | };
126 | }
127 |
128 | protected getPageLengthFromRequest(result: TwitterResponse) {
129 | return result.data.data?.length ?? 0;
130 | }
131 |
132 | protected isFetchLastOver(result: TwitterResponse) {
133 | return !result.data.data?.length || !this.canFetchNextPage(result.data);
134 | }
135 |
136 | protected canFetchNextPage(result: TResult) {
137 | return !!result.meta?.next_token;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/plugins/helpers.ts:
--------------------------------------------------------------------------------
1 | import type { ClientRequestArgs } from 'http';
2 | import type { ClientRequestMaker } from '../client-mixins/request-maker.mixin';
3 | import { ApiPartialResponseError, ApiRequestError, ApiResponseError, IGetHttpRequestArgs, TwitterApiPluginResponseOverride } from '../types';
4 | import type { IComputedHttpRequestArgs } from '../types/request-maker.mixin.types';
5 |
6 | /* Plugin helpers */
7 |
8 | export function hasRequestErrorPlugins(client: ClientRequestMaker) {
9 | if (!client.clientSettings.plugins?.length) {
10 | return false;
11 | }
12 |
13 | for (const plugin of client.clientSettings.plugins) {
14 | if (plugin.onRequestError || plugin.onResponseError) {
15 | return true;
16 | }
17 | }
18 |
19 | return false;
20 | }
21 |
22 | export async function applyResponseHooks(
23 | this: ClientRequestMaker,
24 | requestParams: IGetHttpRequestArgs,
25 | computedParams: IComputedHttpRequestArgs,
26 | requestOptions: Partial,
27 | error: any,
28 | ) {
29 | let override: TwitterApiPluginResponseOverride | undefined;
30 |
31 | if (error instanceof ApiRequestError || error instanceof ApiPartialResponseError) {
32 | override = await this.applyPluginMethod('onRequestError', {
33 | client: this,
34 | url: this.getUrlObjectFromUrlString(requestParams.url),
35 | params: requestParams,
36 | computedParams,
37 | requestOptions,
38 | error,
39 | });
40 | } else if (error instanceof ApiResponseError) {
41 | override = await this.applyPluginMethod('onResponseError', {
42 | client: this,
43 | url: this.getUrlObjectFromUrlString(requestParams.url),
44 | params: requestParams,
45 | computedParams,
46 | requestOptions,
47 | error,
48 | });
49 | }
50 |
51 | if (override && override instanceof TwitterApiPluginResponseOverride) {
52 | return override.value;
53 | }
54 |
55 | return Promise.reject(error);
56 | }
57 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface ITwitterApiV2Settings {
3 | debug: boolean;
4 | deprecationWarnings: boolean;
5 | logger: ITwitterApiV2SettingsLogger;
6 | }
7 |
8 | export interface ITwitterApiV2SettingsLogger {
9 | log(message: string, payload?: any): void;
10 | }
11 |
12 | export const TwitterApiV2Settings: ITwitterApiV2Settings = {
13 | debug: false,
14 | deprecationWarnings: true,
15 | logger: { log: console.log.bind(console) },
16 | };
17 |
--------------------------------------------------------------------------------
/src/stream/TweetStreamEventCombiner.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import { ETwitterStreamEvent } from '../types';
3 | import type TweetStream from './TweetStream';
4 |
5 | type TweetStreamCombinerPayload = { type: 'data', payload: T } | { type: 'error', payload?: any };
6 |
7 | export class TweetStreamEventCombiner extends EventEmitter {
8 | private stack: T[] = [];
9 | private onceNewEvent: (resolver: (value: TweetStreamCombinerPayload) => void) => void;
10 |
11 | constructor(private stream: TweetStream) {
12 | super();
13 |
14 | this.onStreamData = this.onStreamData.bind(this);
15 | this.onStreamError = this.onStreamError.bind(this);
16 | this.onceNewEvent = this.once.bind(this, 'event');
17 |
18 | // Init events from stream
19 | stream.on(ETwitterStreamEvent.Data, this.onStreamData);
20 | // Ignore reconnect errors: Don't close event combiner until connection error/closed
21 | stream.on(ETwitterStreamEvent.ConnectionError, this.onStreamError);
22 | stream.on(ETwitterStreamEvent.TweetParseError, this.onStreamError);
23 | stream.on(ETwitterStreamEvent.ConnectionClosed, this.onStreamError);
24 | }
25 |
26 | /** Returns a new `Promise` that will `resolve` on next event (`data` or any sort of error). */
27 | nextEvent() {
28 | return new Promise(this.onceNewEvent);
29 | }
30 |
31 | /** Returns `true` if there's something in the stack. */
32 | hasStack() {
33 | return this.stack.length > 0;
34 | }
35 |
36 | /** Returns stacked data events, and clean the stack. */
37 | popStack() {
38 | const stack = this.stack;
39 | this.stack = [];
40 | return stack;
41 | }
42 |
43 | /** Cleanup all the listeners attached on stream. */
44 | destroy() {
45 | this.removeAllListeners();
46 | this.stream.off(ETwitterStreamEvent.Data, this.onStreamData);
47 | this.stream.off(ETwitterStreamEvent.ConnectionError, this.onStreamError);
48 | this.stream.off(ETwitterStreamEvent.TweetParseError, this.onStreamError);
49 | this.stream.off(ETwitterStreamEvent.ConnectionClosed, this.onStreamError);
50 | }
51 |
52 | private emitEvent(type: 'data' | 'error', payload?: any) {
53 | this.emit('event', { type, payload });
54 | }
55 |
56 | private onStreamError(payload?: any) {
57 | this.emitEvent('error', payload);
58 | }
59 |
60 | private onStreamData(payload: T) {
61 | this.stack.push(payload);
62 | this.emitEvent('data', payload);
63 | }
64 | }
65 |
66 | export default TweetStreamEventCombiner;
67 |
--------------------------------------------------------------------------------
/src/stream/TweetStreamParser.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | export default class TweetStreamParser extends EventEmitter {
4 | protected currentMessage = '';
5 |
6 | // Code partially belongs to twitter-stream-api for this
7 | // https://github.com/trygve-lie/twitter-stream-api/blob/master/lib/parser.js
8 | push(chunk: string) {
9 | this.currentMessage += chunk;
10 | chunk = this.currentMessage;
11 |
12 | const size = chunk.length;
13 | let start = 0;
14 | let offset = 0;
15 |
16 | while (offset < size) {
17 | // Take [offset, offset+1] inside a new string
18 | if (chunk.slice(offset, offset + 2) === '\r\n') {
19 | // If chunk contains \r\n after current offset,
20 | // parse [start, ..., offset] as a tweet
21 | const piece = chunk.slice(start, offset);
22 | start = offset += 2;
23 |
24 | // If empty object
25 | if (!piece.length) {
26 | continue;
27 | }
28 |
29 | try {
30 | const payload = JSON.parse(piece);
31 |
32 | if (payload) {
33 | this.emit(EStreamParserEvent.ParsedData, payload);
34 | continue;
35 | }
36 | } catch (error) {
37 | this.emit(EStreamParserEvent.ParseError, error);
38 | }
39 | }
40 |
41 | offset++;
42 | }
43 |
44 | this.currentMessage = chunk.slice(start, size);
45 | }
46 |
47 | /** Reset the currently stored message (f.e. on connection reset) */
48 | reset() {
49 | this.currentMessage = '';
50 | }
51 | }
52 |
53 | export enum EStreamParserEvent {
54 | ParsedData = 'parsed data',
55 | ParseError = 'parse error',
56 | }
57 |
--------------------------------------------------------------------------------
/src/test/utils.ts:
--------------------------------------------------------------------------------
1 | import { TwitterApi } from '..';
2 | import * as dotenv from 'dotenv';
3 |
4 | dotenv.config({ path: __dirname + '/../../.env' });
5 |
6 | /** User OAuth 1.0a client */
7 | export function getUserClient(this: any) {
8 | return new TwitterApi({
9 | appKey: process.env.CONSUMER_TOKEN!,
10 | appSecret: process.env.CONSUMER_SECRET!,
11 | accessToken: process.env.OAUTH_TOKEN!,
12 | accessSecret: process.env.OAUTH_SECRET!,
13 | });
14 | }
15 |
16 | export function getUserKeys() {
17 | return {
18 | appKey: process.env.CONSUMER_TOKEN!,
19 | appSecret: process.env.CONSUMER_SECRET!,
20 | accessToken: process.env.OAUTH_TOKEN!,
21 | accessSecret: process.env.OAUTH_SECRET!,
22 | };
23 | }
24 |
25 | export async function sleepTest(ms: number) {
26 | return new Promise(resolve => setTimeout(resolve, ms));
27 | }
28 |
29 | /** User-unlogged OAuth 1.0a client */
30 | export function getRequestClient() {
31 | return new TwitterApi({
32 | appKey: process.env.CONSUMER_TOKEN!,
33 | appSecret: process.env.CONSUMER_SECRET!,
34 | });
35 | }
36 |
37 | export function getRequestKeys() {
38 | return {
39 | appKey: process.env.CONSUMER_TOKEN!,
40 | appSecret: process.env.CONSUMER_SECRET!,
41 | };
42 | }
43 |
44 | // Test auth 1.0a flow
45 | export function getAuthLink(callback: string) {
46 | return getRequestClient().generateAuthLink(callback);
47 | }
48 |
49 | export async function getAccessClient(verifier: string) {
50 | const requestClient = new TwitterApi({
51 | appKey: process.env.CONSUMER_TOKEN!,
52 | appSecret: process.env.CONSUMER_SECRET!,
53 | accessToken: process.env.OAUTH_TOKEN!,
54 | accessSecret: process.env.OAUTH_SECRET!,
55 | });
56 |
57 | const { client } = await requestClient.login(verifier);
58 | return client;
59 | }
60 |
61 | /** App OAuth 2.0 client */
62 | export function getAppClient() {
63 | let requestClient: TwitterApi;
64 |
65 | if (process.env.BEARER_TOKEN) {
66 | requestClient = new TwitterApi(process.env.BEARER_TOKEN);
67 | return Promise.resolve(requestClient);
68 | }
69 | else {
70 | requestClient = new TwitterApi({
71 | appKey: process.env.CONSUMER_TOKEN!,
72 | appSecret: process.env.CONSUMER_SECRET!,
73 | });
74 | return requestClient.appLogin();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/types/auth.types.ts:
--------------------------------------------------------------------------------
1 | import type TwitterApi from '../client';
2 | import { TypeOrArrayOf } from './shared.types';
3 |
4 | export type TOAuth2Scope = 'tweet.read' | 'tweet.write' | 'tweet.moderate.write' | 'users.read' | 'follows.read' | 'follows.write'
5 | | 'offline.access' | 'space.read' | 'mute.read' | 'mute.write' | 'like.read' | 'like.write' | 'list.read' | 'list.write'
6 | | 'block.read' | 'block.write' | 'bookmark.read' | 'bookmark.write' | 'dm.read' | 'dm.write';
7 |
8 | export interface BuildOAuth2RequestLinkArgs {
9 | scope?: TypeOrArrayOf | TypeOrArrayOf;
10 | state?: string;
11 | }
12 |
13 | export interface AccessOAuth2TokenArgs {
14 | /** The same URI given to generate link at previous step. */
15 | redirectUri: string;
16 | /** The code obtained in link generation step. */
17 | codeVerifier: string;
18 | /** The code given by Twitter in query string, after redirection to your callback URL. */
19 | code: string;
20 | }
21 |
22 | export interface AccessOAuth2TokenResult {
23 | token_type: 'bearer';
24 | expires_in: number;
25 | access_token: string;
26 | scope: string;
27 | refresh_token?: string;
28 | }
29 |
30 | export interface RequestTokenArgs {
31 | authAccessType: 'read' | 'write';
32 | linkMode: 'authenticate' | 'authorize';
33 | forceLogin: boolean;
34 | screenName: string;
35 | }
36 |
37 | export interface RequestTokenResult {
38 | oauth_token: string;
39 | oauth_token_secret: string;
40 | oauth_callback_confirmed: 'true';
41 | }
42 |
43 | export interface IOAuth2RequestTokenResult {
44 | url: string;
45 | state: string;
46 | codeVerifier: string;
47 | codeChallenge: string;
48 | }
49 |
50 | export interface AccessTokenResult {
51 | oauth_token: string;
52 | oauth_token_secret: string;
53 | user_id: string;
54 | screen_name: string;
55 | }
56 |
57 | export interface BearerTokenResult {
58 | token_type: 'bearer';
59 | access_token: string;
60 | }
61 |
62 | export interface LoginResult {
63 | userId: string;
64 | screenName: string;
65 | accessToken: string;
66 | accessSecret: string;
67 | client: TwitterApi;
68 | }
69 |
70 | export interface IParsedOAuth2TokenResult {
71 | client: TwitterApi;
72 | expiresIn: number;
73 | accessToken: string;
74 | scope: TOAuth2Scope[];
75 | refreshToken?: string;
76 | }
77 |
--------------------------------------------------------------------------------
/src/types/client.types.ts:
--------------------------------------------------------------------------------
1 | import type { Agent } from 'http';
2 | import type { ITwitterApiClientPlugin } from './plugins';
3 | import type { TRequestCompressionLevel } from './request-maker.mixin.types';
4 |
5 | export enum ETwitterStreamEvent {
6 | Connected = 'connected',
7 | ConnectError = 'connect error',
8 | ConnectionError = 'connection error',
9 | ConnectionClosed = 'connection closed',
10 | ConnectionLost = 'connection lost',
11 | ReconnectAttempt = 'reconnect attempt',
12 | Reconnected = 'reconnected',
13 | ReconnectError = 'reconnect error',
14 | ReconnectLimitExceeded = 'reconnect limit exceeded',
15 | DataKeepAlive = 'data keep-alive',
16 | Data = 'data event content',
17 | DataError = 'data twitter error',
18 | TweetParseError = 'data tweet parse error',
19 | Error = 'stream error',
20 | }
21 |
22 | export interface TwitterApiTokens {
23 | appKey: string;
24 | appSecret: string;
25 | accessToken?: string;
26 | accessSecret?: string;
27 | }
28 |
29 | export interface TwitterApiOAuth2Init {
30 | clientId: string;
31 | clientSecret?: string;
32 | }
33 |
34 | export interface TwitterApiBasicAuth {
35 | username: string;
36 | password: string;
37 | }
38 |
39 | export interface IClientTokenBearer {
40 | bearerToken: string;
41 | type: 'oauth2';
42 | }
43 |
44 | export interface IClientOAuth2UserClient {
45 | clientId: string;
46 | type: 'oauth2-user';
47 | }
48 |
49 | export interface IClientTokenBasic {
50 | token: string;
51 | type: 'basic';
52 | }
53 |
54 | export interface IClientTokenOauth {
55 | appKey: string;
56 | appSecret: string;
57 | accessToken?: string;
58 | accessSecret?: string;
59 | type: 'oauth-1.0a';
60 | }
61 |
62 | export interface IClientTokenNone {
63 | type: 'none';
64 | }
65 |
66 | export type TClientTokens = IClientTokenNone | IClientTokenBearer | IClientTokenOauth | IClientTokenBasic | IClientOAuth2UserClient;
67 |
68 | export interface IClientSettings {
69 | /** Used to send HTTPS requests. This is mostly used to make requests work behind a proxy. */
70 | httpAgent: Agent;
71 | plugins: ITwitterApiClientPlugin[];
72 | compression: TRequestCompressionLevel;
73 | }
74 |
--------------------------------------------------------------------------------
/src/types/entities.types.ts:
--------------------------------------------------------------------------------
1 | // Entities
2 | export interface Entity {
3 | start: number;
4 | end: number;
5 | }
6 |
7 | export interface UrlEntity extends Entity {
8 | url: string; // https;//t.co/...
9 | expanded_url: string; // https://unfollow.ninja/
10 | display_url: string; // unfollow.ninja
11 | }
12 |
13 | export interface HashtagEntity extends Entity {
14 | tag: string;
15 | }
16 |
17 | export interface CashtagEntity extends Entity {
18 | tag: string;
19 | }
20 |
21 | export interface MentionEntity extends Entity {
22 | username: string;
23 | }
24 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './v1';
2 | export * from './v2';
3 | export * from './errors.types';
4 | export * from './responses.types';
5 | export * from './client.types';
6 | export * from './auth.types';
7 | export * from './plugins';
8 | export { IGetHttpRequestArgs } from './request-maker.mixin.types';
9 |
--------------------------------------------------------------------------------
/src/types/plugins/client.plugins.types.ts:
--------------------------------------------------------------------------------
1 | import type { ClientRequestArgs } from 'http';
2 | import type { IGetHttpRequestArgs } from '../request-maker.mixin.types';
3 | import type { TwitterResponse } from '../responses.types';
4 | import type { IComputedHttpRequestArgs } from '../request-maker.mixin.types';
5 | import type { IOAuth2RequestTokenResult, RequestTokenResult } from '../auth.types';
6 | import type { PromiseOrType } from '../shared.types';
7 | import type { ApiResponseError, ApiRequestError, ApiPartialResponseError } from '../errors.types';
8 | import type { ClientRequestMaker } from '../../client-mixins/request-maker.mixin';
9 |
10 | export class TwitterApiPluginResponseOverride {
11 | constructor(public value: any) {}
12 | }
13 |
14 | export interface ITwitterApiClientPlugin {
15 | // Classic requests
16 | onBeforeRequestConfig?: TTwitterApiBeforeRequestConfigHook;
17 | onBeforeRequest?: TTwitterApiBeforeRequestHook;
18 | onAfterRequest?: TTwitterApiAfterRequestHook;
19 | // Request errors
20 | onRequestError?: TTwitterApiRequestErrorHook;
21 | onResponseError?: TTwitterApiResponseErrorHook;
22 | // Stream requests
23 | onBeforeStreamRequestConfig?: TTwitterApiBeforeStreamRequestConfigHook;
24 | // Request token
25 | onOAuth1RequestToken?: TTwitterApiAfterOAuth1RequestTokenHook;
26 | onOAuth2RequestToken?: TTwitterApiAfterOAuth2RequestTokenHook;
27 | }
28 |
29 | // - Requests -
30 |
31 | export interface ITwitterApiBeforeRequestConfigHookArgs {
32 | client: ClientRequestMaker;
33 | url: URL;
34 | params: IGetHttpRequestArgs;
35 | }
36 |
37 | export interface ITwitterApiBeforeRequestHookArgs extends ITwitterApiBeforeRequestConfigHookArgs {
38 | computedParams: IComputedHttpRequestArgs;
39 | requestOptions: Partial
40 | }
41 |
42 | export interface ITwitterApiAfterRequestHookArgs extends ITwitterApiBeforeRequestHookArgs {
43 | response: TwitterResponse;
44 | }
45 |
46 | export interface ITwitterApiRequestErrorHookArgs extends ITwitterApiBeforeRequestHookArgs {
47 | error: ApiRequestError | ApiPartialResponseError;
48 | }
49 |
50 | export interface ITwitterApiResponseErrorHookArgs extends ITwitterApiBeforeRequestHookArgs {
51 | error: ApiResponseError;
52 | }
53 |
54 | export type TTwitterApiBeforeRequestConfigHook = (args: ITwitterApiBeforeRequestConfigHookArgs) => PromiseOrType | void>;
55 | export type TTwitterApiBeforeRequestHook = (args: ITwitterApiBeforeRequestHookArgs) => void | Promise;
56 | export type TTwitterApiAfterRequestHook = (args: ITwitterApiAfterRequestHookArgs) => PromiseOrType;
57 |
58 | export type TTwitterApiRequestErrorHook = (args: ITwitterApiRequestErrorHookArgs) => PromiseOrType;
59 | export type TTwitterApiResponseErrorHook = (args: ITwitterApiResponseErrorHookArgs) => PromiseOrType;
60 |
61 | export type TTwitterApiBeforeStreamRequestConfigHook = (args: ITwitterApiBeforeRequestConfigHookArgs) => void;
62 |
63 |
64 | // - Auth -
65 |
66 | export interface ITwitterApiAfterOAuth1RequestTokenHookArgs {
67 | client: ClientRequestMaker;
68 | url: string;
69 | oauthResult: RequestTokenResult;
70 | }
71 |
72 | export interface ITwitterApiAfterOAuth2RequestTokenHookArgs {
73 | client: ClientRequestMaker;
74 | result: IOAuth2RequestTokenResult;
75 | redirectUri: string;
76 | }
77 |
78 | export type TTwitterApiAfterOAuth1RequestTokenHook = (args: ITwitterApiAfterOAuth1RequestTokenHookArgs) => void | Promise;
79 | export type TTwitterApiAfterOAuth2RequestTokenHook = (args: ITwitterApiAfterOAuth2RequestTokenHookArgs) => void | Promise;
80 |
--------------------------------------------------------------------------------
/src/types/plugins/index.ts:
--------------------------------------------------------------------------------
1 |
2 | export * from './client.plugins.types';
3 |
--------------------------------------------------------------------------------
/src/types/request-maker.mixin.types.ts:
--------------------------------------------------------------------------------
1 | import type { RequestOptions } from 'https';
2 | import type { TwitterApiBasicAuth, TwitterApiOAuth2Init, TwitterApiTokens } from './client.types';
3 | import type { TwitterRateLimit } from './responses.types';
4 |
5 | export type TRequestDebuggerHandlerEvent = 'abort' | 'close' | 'socket' | 'socket-error' | 'socket-connect'
6 | | 'socket-close' | 'socket-end' | 'socket-lookup' | 'socket-timeout' | 'request-error'
7 | | 'response' | 'response-aborted' | 'response-error' | 'response-close' | 'response-end';
8 | export type TRequestDebuggerHandler = (event: TRequestDebuggerHandlerEvent, data?: any) => void;
9 |
10 | /**
11 | * Request compression level. `true` means `'brotli'`, `false` means `'identity'`.
12 | * When `brotli` is unavailable (f.e. in streams), it will fallback to `gzip`.
13 | */
14 | export type TRequestCompressionLevel = boolean | 'brotli' | 'gzip' | 'deflate' | 'identity';
15 |
16 | export type TRequestFullData = {
17 | url: URL,
18 | options: RequestOptions,
19 | body?: any,
20 | rateLimitSaver?: (rateLimit: TwitterRateLimit) => any,
21 | requestEventDebugHandler?: TRequestDebuggerHandler,
22 | compression?: TRequestCompressionLevel,
23 | forceParseMode?: TResponseParseMode,
24 | };
25 |
26 | export type TRequestFullStreamData = TRequestFullData & { payloadIsError?: (data: any) => boolean };
27 | export type TRequestQuery = Record;
28 | export type TRequestStringQuery = Record;
29 | export type TRequestBody = Record | Buffer;
30 | export type TBodyMode = 'json' | 'url' | 'form-data' | 'raw';
31 | export type TResponseParseMode = 'json' | 'url' | 'text' | 'buffer' | 'none';
32 |
33 | export interface IWriteAuthHeadersArgs {
34 | headers: Record;
35 | bodyInSignature: boolean;
36 | url: URL;
37 | method: string;
38 | query: TRequestQuery;
39 | body: TRequestBody;
40 | }
41 |
42 | export interface IGetHttpRequestArgs {
43 | url: string;
44 | method: string;
45 | query?: TRequestQuery;
46 | /** The URL parameters, if you specify an endpoint with `:id`, for example. */
47 | params?: TRequestQuery;
48 | body?: TRequestBody;
49 | headers?: Record;
50 | forceBodyMode?: TBodyMode;
51 | forceParseMode?: TResponseParseMode;
52 | enableAuth?: boolean;
53 | enableRateLimitSave?: boolean;
54 | timeout?: number;
55 | requestEventDebugHandler?: TRequestDebuggerHandler;
56 | compression?: TRequestCompressionLevel;
57 | }
58 |
59 | export interface IComputedHttpRequestArgs {
60 | rawUrl: string;
61 | url: URL;
62 | method: string;
63 | headers: Record;
64 | body: string | Buffer | undefined;
65 | }
66 |
67 | export interface IGetStreamRequestArgs {
68 | payloadIsError?: (data: any) => boolean;
69 | autoConnect?: boolean;
70 | }
71 |
72 | export interface IGetStreamRequestArgsAsync {
73 | payloadIsError?: (data: any) => boolean;
74 | autoConnect?: true;
75 | }
76 |
77 | export interface IGetStreamRequestArgsSync {
78 | payloadIsError?: (data: any) => boolean;
79 | autoConnect: false;
80 | }
81 |
82 | export type TCustomizableRequestArgs = Pick;
83 |
84 | export type TAcceptedInitToken = TwitterApiTokens | TwitterApiOAuth2Init | TwitterApiBasicAuth | string;
85 |
--------------------------------------------------------------------------------
/src/types/responses.types.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingHttpHeaders } from 'http';
2 |
3 | export interface TwitterResponse {
4 | headers: IncomingHttpHeaders;
5 | data: T;
6 | rateLimit?: TwitterRateLimit;
7 | }
8 |
9 | export interface SingleTwitterRateLimit {
10 | limit: number;
11 | reset: number;
12 | remaining: number;
13 | }
14 |
15 | export interface TwitterRateLimit extends SingleTwitterRateLimit {
16 | day?: SingleTwitterRateLimit;
17 | }
18 |
--------------------------------------------------------------------------------
/src/types/shared.types.ts:
--------------------------------------------------------------------------------
1 | export type NumberString = number | string;
2 | export type BooleanString = boolean | string;
3 | export type TypeOrArrayOf = T | T[];
4 | export type PromiseOrType = T | Promise;
5 |
--------------------------------------------------------------------------------
/src/types/v1/dev-utilities.v1.types.ts:
--------------------------------------------------------------------------------
1 |
2 | export type TAppRateLimitResourceV1 = 'help' | 'statuses' | 'friends' | 'followers'
3 | | 'users' | 'search' | 'trends' | 'favorites' | 'friendships' | 'direct_messages'
4 | | 'lists' | 'geo' | 'account' | 'application' | 'account_activity';
5 |
6 | export interface AppRateLimitV1Result {
7 | rate_limit_context: { access_token: string };
8 | resources: {
9 | [TResourceName in TAppRateLimitResourceV1]?: {
10 | [resourceEndpoint: string]: AppRateLimitEndpointV1;
11 | };
12 | };
13 | }
14 |
15 | export interface AppRateLimitEndpointV1 {
16 | limit: number;
17 | remaining: number;
18 | reset: number;
19 | }
20 |
21 | export interface HelpLanguageV1Result {
22 | code: string;
23 | status: string;
24 | name: string;
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/v1/dm.v1.types.ts:
--------------------------------------------------------------------------------
1 | import { CoordinateV1, MediaEntityV1, TweetEntitiesV1 } from './entities.v1.types';
2 |
3 | // Creation of DMs
4 | export enum EDirectMessageEventTypeV1 {
5 | Create = 'message_create',
6 | WelcomeCreate = 'welcome_message',
7 | }
8 |
9 | export interface MessageCreateEventV1 {
10 | target: {
11 | recipient_id: string;
12 | };
13 | message_data: MessageCreateDataV1;
14 | custom_profile_id?: string;
15 | }
16 |
17 | export interface MessageCreateDataV1 {
18 | text: string;
19 | attachment?: MessageCreateAttachmentV1;
20 | quick_reply?: MessageCreateQuickReplyV1;
21 | ctas?: MessageCreateCtaV1[];
22 | }
23 |
24 | export interface MessageCreateCtaV1 {
25 | type: 'web_url';
26 | url: string;
27 | label: string;
28 | /** Only when messages are retrieved from API */
29 | tco_url?: string;
30 | }
31 |
32 | export interface MessageCreateQuickReplyOptionV1 {
33 | label: string;
34 | description?: string;
35 | metadata?: string;
36 | }
37 |
38 | export interface MessageCreateQuickReplyV1 {
39 | type: 'options';
40 | options: MessageCreateQuickReplyOptionV1[];
41 | }
42 |
43 | export type MessageCreateAttachmentV1 = {
44 | type: 'media';
45 | media: { id: string };
46 | } | {
47 | type: 'location';
48 | location: MessageCreateLocationV1;
49 | };
50 |
51 | export type MessageCreateLocationV1 = {
52 | type: 'shared_coordinate';
53 | shared_coordinate: {
54 | coordinates: CoordinateV1;
55 | };
56 | } | {
57 | type: 'shared_place';
58 | shared_place: {
59 | place: { id: string };
60 | };
61 | };
62 |
63 | // - Params -
64 |
65 | export interface SendDMV1Params extends MessageCreateDataV1 {
66 | recipient_id: string;
67 | custom_profile_id?: string;
68 | }
69 |
70 | export interface CreateDMEventV1Args {
71 | event: CreateDMEventV1;
72 | }
73 |
74 | export interface CreateDMEventV1 {
75 | type: EDirectMessageEventTypeV1;
76 | [EDirectMessageEventTypeV1.Create]?: MessageCreateEventV1;
77 | }
78 |
79 | export interface GetDmListV1Args {
80 | cursor?: string;
81 | count?: number;
82 | }
83 |
84 | export interface CreateWelcomeDMEventV1Args {
85 | [EDirectMessageEventTypeV1.WelcomeCreate]: {
86 | name: string;
87 | message_data: MessageCreateDataV1;
88 | };
89 | }
90 |
91 | // - Responses -
92 |
93 | // Responses of DMs (payload replied by API)
94 | export interface DirectMessageCreateV1 extends ReceivedDirectMessageEventV1 {
95 | type: EDirectMessageEventTypeV1.Create;
96 | [EDirectMessageEventTypeV1.Create]: ReceivedMessageCreateEventV1;
97 | }
98 |
99 | export interface ReceivedDirectMessageEventV1 {
100 | type: EDirectMessageEventTypeV1;
101 | id: string;
102 | created_timestamp: string;
103 | initiated_via?: { tweet_id?: string, welcome_message_id?: string };
104 | }
105 |
106 | export interface ReceivedMessageCreateEventV1 {
107 | target: { recipient_id: string };
108 | sender_id: string;
109 | source_app_id: string;
110 | message_data: ReceivedMessageCreateDataV1;
111 | custom_profile_id?: string;
112 | }
113 |
114 | export interface ReceivedMessageCreateDataV1 {
115 | text: string;
116 | entities: TweetEntitiesV1;
117 | quick_reply_response?: { type: 'options', metadata?: string };
118 | attachment?: { type: 'media', media: MediaEntityV1 };
119 | quick_reply?: MessageCreateQuickReplyV1;
120 | ctas?: MessageCreateCtaV1[];
121 | }
122 | // -- end dm event entity
123 |
124 | export interface ReceivedWelcomeDMCreateEventV1 {
125 | id: string;
126 | created_timestamp: string;
127 | message_data: ReceivedMessageCreateDataV1;
128 | name?: string;
129 | }
130 |
131 | export type DirectMessageCreateV1Result = { event: DirectMessageCreateV1 } & ReceivedDMAppsV1;
132 |
133 | export interface ReceivedDMAppsV1 {
134 | apps?: ReceivedDMAppListV1;
135 | }
136 |
137 | // TODO add other events
138 | export type TReceivedDMEvent = DirectMessageCreateV1;
139 |
140 | // GET direct_messages/events/show
141 | export interface ReceivedDMEventV1 extends ReceivedDMAppsV1 {
142 | event: TReceivedDMEvent;
143 | }
144 |
145 | // GET direct_messages/events/list
146 | export interface ReceivedDMEventsV1 extends ReceivedDMAppsV1 {
147 | next_cursor?: string;
148 | events: TReceivedDMEvent[];
149 | }
150 |
151 | export interface ReceivedDMAppListV1 {
152 | [appId: string]: {
153 | id: string;
154 | name: string;
155 | url: string;
156 | };
157 | }
158 |
159 | // -- Welcome messages --
160 |
161 | // POST direct_messages/welcome_messages/new
162 | export interface WelcomeDirectMessageCreateV1Result extends ReceivedDMAppsV1 {
163 | [EDirectMessageEventTypeV1.WelcomeCreate]: ReceivedWelcomeDMCreateEventV1;
164 | name?: string;
165 | }
166 |
167 | // GET direct_messages/welcome_messages/list
168 | export interface WelcomeDirectMessageListV1Result extends ReceivedDMAppsV1 {
169 | next_cursor?: string;
170 | welcome_messages: ReceivedWelcomeDMCreateEventV1[];
171 | }
172 |
173 | // -- Welcome message rules --
174 |
175 | export interface CreateWelcomeDmRuleV1 {
176 | welcome_message_id: string;
177 | }
178 |
179 | export interface WelcomeDmRuleV1 extends CreateWelcomeDmRuleV1 {
180 | id: string;
181 | created_timestamp: string;
182 | }
183 |
184 | export interface WelcomeDmRuleV1Result {
185 | welcome_message_rule: WelcomeDmRuleV1;
186 | }
187 |
188 | export interface WelcomeDmRuleListV1Result {
189 | next_cursor?: string;
190 | welcome_message_rules?: WelcomeDmRuleV1[];
191 | }
192 |
--------------------------------------------------------------------------------
/src/types/v1/entities.v1.types.ts:
--------------------------------------------------------------------------------
1 | export interface TweetEntitiesV1 {
2 | hashtags?: HashtagEntityV1[];
3 | urls?: UrlEntityV1[];
4 | user_mentions?: MentionEntityV1[];
5 | symbols?: SymbolEntityV1[];
6 | media?: MediaEntityV1[];
7 | polls?: [PollEntityV1];
8 | }
9 |
10 | export interface TweetExtendedEntitiesV1 {
11 | media?: MediaEntityV1[];
12 | }
13 |
14 | export interface UserEntitiesV1 {
15 | url?: { urls: UrlEntityV1[]; };
16 | description?: { urls: UrlEntityV1[]; };
17 | }
18 |
19 | export interface UrlEntityV1 {
20 | display_url: string;
21 | expanded_url: string;
22 | indices: [number, number];
23 | url: string;
24 | unwound?: {
25 | url: string;
26 | status: number;
27 | title: string;
28 | description: string;
29 | };
30 | }
31 |
32 | export interface MediaEntityV1 {
33 | display_url: string;
34 | expanded_url: string;
35 | url: string;
36 | id: number;
37 | id_str: string;
38 | indices: [number, number];
39 | media_url: string;
40 | media_url_https: string;
41 | sizes: MediaSizesV1;
42 | source_status_id: number;
43 | source_status_id_str: string;
44 | source_user_id: number;
45 | source_user_id_str: string;
46 | type: 'photo' | 'video' | 'animated_gif';
47 | video_info?: MediaVideoInfoV1;
48 | additional_media_info?: AdditionalMediaInfoV1;
49 | ext_alt_text?: string;
50 | }
51 |
52 | export interface MediaVideoInfoV1 {
53 | aspect_ratio: [number, number];
54 | duration_millis: number;
55 | variants: {
56 | bitrate: number;
57 | content_type: string;
58 | url: string;
59 | }[];
60 | }
61 |
62 | export interface AdditionalMediaInfoV1 {
63 | title: string;
64 | description: string;
65 | embeddable: boolean;
66 | monetizable: boolean;
67 | }
68 |
69 | export interface MediaSizesV1 {
70 | thumb: MediaSizeObjectV1;
71 | large: MediaSizeObjectV1;
72 | medium: MediaSizeObjectV1;
73 | small: MediaSizeObjectV1;
74 | }
75 |
76 | export interface MediaSizeObjectV1 {
77 | w: number;
78 | h: number;
79 | resize: 'crop' | 'fit';
80 | }
81 |
82 | export interface HashtagEntityV1 {
83 | indices: [number, number];
84 | text: string;
85 | }
86 |
87 | export interface MentionEntityV1 {
88 | id: number;
89 | id_str: string;
90 | indices: [number, number];
91 | name: string;
92 | screen_name: string;
93 | }
94 |
95 | export interface SymbolEntityV1 {
96 | indices: [number, number];
97 | text: string;
98 | }
99 |
100 | export interface PollEntityV1 {
101 | options: PollPositionV1[];
102 | end_datetime: string;
103 | duration_minutes: number;
104 | }
105 |
106 | export interface PollPositionV1 {
107 | position: number;
108 | text: string;
109 | }
110 |
111 | /** See GeoJSON. */
112 | export interface CoordinateV1 {
113 | coordinates: number[] | number[][];
114 | type: string;
115 | }
116 |
117 | export interface PlaceV1 {
118 | full_name: string;
119 | id: string;
120 | url: string;
121 | country: string;
122 | country_code: string;
123 | bounding_box: CoordinateV1;
124 | name: string;
125 | place_type: string;
126 | contained_within?: PlaceV1[];
127 | geometry?: any;
128 | polylines?: number[];
129 | centroid?: number[];
130 | attributes?: {
131 | geotagCount: string;
132 | [geoTagId: string]: string;
133 | };
134 | }
135 |
136 | export interface TrendV1 {
137 | name: string;
138 | url: string;
139 | promoted_content?: boolean;
140 | query: string;
141 | tweet_volume: number;
142 | }
143 |
144 | export interface TrendLocationV1 {
145 | name: string;
146 | woeid: number;
147 | url?: string;
148 | placeType?: { code: number; name: string; };
149 | parentid?: number;
150 | country?: string;
151 | countryCode?: string;
152 | }
153 |
--------------------------------------------------------------------------------
/src/types/v1/geo.v1.types.ts:
--------------------------------------------------------------------------------
1 | import { PlaceV1 } from './entities.v1.types';
2 |
3 | export interface ReverseGeoCodeV1Params {
4 | lat: number;
5 | long: number;
6 | accuracy?: string;
7 | granularity?: 'city' | 'neighborhood' | 'country' | 'admin';
8 | max_results?: number;
9 | }
10 |
11 | export interface ReverseGeoCodeV1Result {
12 | query: {
13 | params: Partial;
14 | type: string;
15 | url: string;
16 | };
17 | result: { places: PlaceV1[] };
18 | }
19 |
20 |
21 | export interface SearchGeoV1Params extends Partial {
22 | ip?: string;
23 | query?: string;
24 | }
25 |
26 | export interface SearchGeoV1Result {
27 | query: {
28 | params: SearchGeoV1Params;
29 | type: string;
30 | url: string;
31 | };
32 | result: { places: PlaceV1[] };
33 | }
34 |
--------------------------------------------------------------------------------
/src/types/v1/index.ts:
--------------------------------------------------------------------------------
1 | export * from './streaming.v1.types';
2 | export * from './tweet.v1.types';
3 | export * from './entities.v1.types';
4 | export * from './user.v1.types';
5 | export * from './dev-utilities.v1.types';
6 | export * from './geo.v1.types';
7 | export * from './trends.v1.types';
8 | export * from './dm.v1.types';
9 | export * from './list.v1.types';
10 |
--------------------------------------------------------------------------------
/src/types/v1/list.v1.types.ts:
--------------------------------------------------------------------------------
1 | import { TweetV1TimelineParams } from './tweet.v1.types';
2 | import { UserV1 } from './user.v1.types';
3 |
4 | export interface ListV1 {
5 | id: number;
6 | id_str: string;
7 | slug: string;
8 | name: string;
9 | full_name: string;
10 | created_at: string;
11 | description: string;
12 | uri: string;
13 | subscriber_count: number;
14 | member_count: number;
15 | mode: 'public' | 'private';
16 | user: UserV1;
17 | }
18 |
19 |
20 | // PARAMS
21 |
22 | type TUserOrScreenName = { user_id: string } | { screen_name: string };
23 |
24 | export type ListListsV1Params = Partial & { reverse?: boolean };
25 |
26 | export interface GetListV1Params {
27 | list_id?: string;
28 | slug?: string;
29 | owner_screen_name?: string;
30 | owner_id?: string;
31 | }
32 |
33 | export interface ListMemberShowV1Params extends GetListV1Params {
34 | include_entities?: boolean;
35 | skip_status?: boolean;
36 | user_id?: string;
37 | screen_name?: string;
38 | tweet_mode?: 'compat' | 'extended';
39 | }
40 |
41 | export interface ListMembersV1Params extends GetListV1Params {
42 | count?: number;
43 | cursor?: string;
44 | include_entities?: boolean;
45 | skip_status?: boolean;
46 | tweet_mode?: 'compat' | 'extended';
47 | }
48 |
49 | export interface ListOwnershipsV1Params {
50 | user_id?: string;
51 | screen_name?: string;
52 | count?: number;
53 | cursor?: string;
54 | include_entities?: boolean;
55 | skip_status?: boolean;
56 | tweet_mode?: 'compat' | 'extended';
57 | }
58 |
59 | export type ListSubscriptionsV1Params = ListOwnershipsV1Params;
60 |
61 | export interface ListMembershipsV1Params extends ListOwnershipsV1Params {
62 | filter_to_owned_lists?: boolean;
63 | }
64 |
65 | export interface ListStatusesV1Params extends TweetV1TimelineParams, GetListV1Params {
66 | include_rts?: boolean;
67 | }
68 |
69 | export interface ListCreateV1Params {
70 | name: string;
71 | mode?: 'public' | 'private';
72 | description?: string;
73 | tweet_mode?: 'compat' | 'extended';
74 | }
75 |
76 | export interface AddOrRemoveListMembersV1Params extends GetListV1Params {
77 | user_id?: string | string[];
78 | screen_name?: string | string[];
79 | }
80 |
81 | export interface UpdateListV1Params extends Partial, GetListV1Params {}
82 |
83 | // RESULTS
84 |
85 | export interface DoubleEndedListsCursorV1Result {
86 | next_cursor?: string;
87 | next_cursor_str?: string;
88 | previous_cursor?: string;
89 | previous_cursor_str?: string;
90 | lists: ListV1[];
91 | }
92 |
93 | export interface DoubleEndedUsersCursorV1Result {
94 | next_cursor?: string;
95 | next_cursor_str?: string;
96 | previous_cursor?: string;
97 | previous_cursor_str?: string;
98 | users: UserV1[];
99 | }
100 |
--------------------------------------------------------------------------------
/src/types/v1/streaming.v1.types.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOrArrayOf } from '../shared.types';
2 |
3 | export interface AskTweetStreamV1Params {
4 | tweet_mode?: 'extended' | 'compat';
5 | /** Specifies whether stall warnings should be delivered. */
6 | stall_warnings: boolean;
7 | }
8 |
9 | /**
10 | * See https://developer.x.com/en/docs/twitter-api/v1/tweets/filter-realtime/guides/basic-stream-parameters
11 | * for detailed documentation.
12 | */
13 | export interface FilterStreamV1Params extends AskTweetStreamV1Params {
14 | /** A list of user IDs, indicating the users to return statuses for in the stream. */
15 | follow: TypeOrArrayOf<(string | BigInt)>;
16 | /** Keywords to track. */
17 | track: TypeOrArrayOf;
18 | /** Specifies a set of bounding boxes to track. */
19 | locations: TypeOrArrayOf<{ lng: string, lat: string }>;
20 | /** Specifies whether stall warnings should be delivered. */
21 | stall_warnings: boolean;
22 |
23 | [otherParameter: string]: any;
24 | }
25 |
26 | export interface SampleStreamV1Params extends AskTweetStreamV1Params {
27 | [otherParameter: string]: any;
28 | }
29 |
--------------------------------------------------------------------------------
/src/types/v1/trends.v1.types.ts:
--------------------------------------------------------------------------------
1 | import { TrendLocationV1, TrendV1 } from './entities.v1.types';
2 |
3 | export interface TrendsPlaceV1Params {
4 | exclude: string;
5 | }
6 |
7 | export interface TrendMatchV1 {
8 | trends: TrendV1[];
9 | as_of: string;
10 | created_at: string;
11 | locations: TrendLocationV1[];
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/v1/tweet.v1.types.ts:
--------------------------------------------------------------------------------
1 | import type * as fs from 'fs';
2 | import type { BooleanString, NumberString } from '../shared.types';
3 | import { CoordinateV1, PlaceV1, TweetEntitiesV1, TweetExtendedEntitiesV1 } from './entities.v1.types';
4 | import { UserV1 } from './user.v1.types';
5 |
6 | // - Entity -
7 |
8 | export interface TweetV1 {
9 | created_at: string;
10 | id: number;
11 | id_str: string;
12 | text: string;
13 | full_text?: string;
14 | source: string;
15 | truncated: boolean;
16 | in_reply_to_status_id: number | null;
17 | in_reply_to_status_id_str: string | null;
18 | in_reply_to_user_id: number | null;
19 | in_reply_to_user_id_str: string | null;
20 | in_reply_to_screen_name: string | null;
21 | user: UserV1;
22 | coordinates: CoordinateV1 | null;
23 | place: PlaceV1 | null;
24 | quoted_status_id: number;
25 | quoted_status_id_str: string;
26 | is_quote_status: boolean;
27 | quoted_status?: TweetV1;
28 | retweeted_status?: TweetV1;
29 | quote_count?: number;
30 | reply_count?: number;
31 | retweet_count: number;
32 | favorite_count: number;
33 | entities: TweetEntitiesV1;
34 | extended_entities?: TweetExtendedEntitiesV1;
35 | favorited: boolean | null;
36 | retweeted: boolean;
37 | possibly_sensitive: boolean | null;
38 | filter_level: 'none' | 'low' | 'medium' | 'high';
39 | lang: string;
40 | display_text_range?: [number, number];
41 |
42 | // Additional attributes
43 | current_user_retweet?: { id: number, id_str: string };
44 | withheld_copyright?: boolean;
45 | withheld_in_countries?: string[];
46 | withheld_scope?: string;
47 | card_uri?: string;
48 | }
49 |
50 | // - Params -
51 |
52 | export interface TweetShowV1Params {
53 | tweet_mode?: 'compat' | 'extended';
54 | id?: string;
55 | trim_user?: boolean;
56 | include_my_retweet?: boolean;
57 | include_entities?: boolean;
58 | include_ext_alt_text?: boolean;
59 | include_card_uri?: boolean;
60 | }
61 |
62 | export type TweetLookupV1Params = {
63 | id?: string | string[];
64 | map?: boolean;
65 | } & Omit;
66 | export type TweetLookupNoMapV1Params = TweetLookupV1Params & { map?: false };
67 | export type TweetLookupMapV1Params = TweetLookupV1Params & { map: true };
68 |
69 | export interface AskTweetV1Params {
70 | tweet_mode?: 'extended' | 'compat';
71 | include_entities?: boolean;
72 | trim_user?: boolean;
73 | }
74 |
75 | export interface TweetV1TimelineParams extends AskTweetV1Params {
76 | count?: number;
77 | since_id?: string;
78 | max_id?: string;
79 | exclude_replies?: boolean;
80 | }
81 |
82 | export interface TweetV1UserTimelineParams extends TweetV1TimelineParams {
83 | user_id?: string;
84 | screen_name?: string;
85 | include_rts?: boolean;
86 | }
87 |
88 | export interface SendTweetV1Params extends AskTweetV1Params {
89 | status: string;
90 | in_reply_to_status_id?: string;
91 | auto_populate_reply_metadata?: BooleanString;
92 | exclude_reply_user_ids?: string;
93 | attachment_url?: string;
94 | media_ids?: string | string[];
95 | possibly_sensitive?: BooleanString;
96 | lat?: NumberString;
97 | long?: NumberString;
98 | display_coordinates?: BooleanString;
99 | enable_dmcommands?: BooleanString;
100 | fail_dmcommands?: BooleanString;
101 | card_uri?: string;
102 | place_id?: string;
103 | }
104 |
105 | export type TUploadTypeV1 = 'mp4' | 'longmp4' | 'mov' | 'gif' | 'jpg' | 'png' | 'srt' | 'webp';
106 |
107 | export enum EUploadMimeType {
108 | Jpeg = 'image/jpeg',
109 | Mp4 = 'video/mp4',
110 | Mov = 'video/quicktime',
111 | Gif = 'image/gif',
112 | Png = 'image/png',
113 | Srt = 'text/plain',
114 | Webp = 'image/webp'
115 | }
116 |
117 | export interface UploadMediaV1Params {
118 | /** @deprecated Directly use `mimeType` parameter with one of the allowed MIME types in `EUploadMimeType`. */
119 | type: TUploadTypeV1 | string;
120 | mimeType: EUploadMimeType | string;
121 | target: 'tweet' | 'dm';
122 | chunkLength: number;
123 | shared: boolean;
124 | longVideo: boolean;
125 | additionalOwners: string | string[];
126 | maxConcurrentUploads: number;
127 | }
128 |
129 | export interface MediaMetadataV1Params {
130 | alt_text?: { text: string };
131 | }
132 |
133 | export interface MediaSubtitleV1Param {
134 | media_id: string;
135 | language_code: string;
136 | display_name: string;
137 | }
138 |
139 | /**
140 | * Link to a file that is usable as media.
141 | * - `string`: File path
142 | * - `Buffer`: File content, as binary buffer
143 | * - `fs.promises.FileHandle`: Opened file with `fs.promises`
144 | * - `number`: Opened file with `fs` classic functions
145 | */
146 | export type TUploadableMedia = string | Buffer | fs.promises.FileHandle | number;
147 |
148 | export interface OembedTweetV1Params {
149 | url: string;
150 | maxwidth?: number;
151 | hide_media?: boolean;
152 | hide_thread?: boolean;
153 | omit_script?: boolean;
154 | align?: 'left' | 'right' | 'center' | 'none';
155 | related?: string;
156 | lang?: string;
157 | theme?: 'light' | 'dark';
158 | link_color?: string;
159 | widget_type?: 'video';
160 | dnt?: boolean;
161 | }
162 |
163 | // - Results -
164 |
165 | export type TweetV1TimelineResult = TweetV1[];
166 |
167 | export interface InitMediaV1Result {
168 | media_id: number;
169 | media_id_string: string;
170 | size: number;
171 | expires_after_secs: number;
172 | image: {
173 | image_type: string;
174 | w: number;
175 | h: number;
176 | };
177 | }
178 |
179 | export interface MediaStatusV1Result {
180 | media_id: number;
181 | media_id_string: string;
182 | size: number,
183 | expires_after_secs: number;
184 | video?: {
185 | video_type: string;
186 | };
187 | processing_info?: {
188 | state: 'pending' | 'failed' | 'succeeded' | 'in_progress';
189 | check_after_secs?: number;
190 | progress_percent?: number;
191 | error?: {
192 | code: number;
193 | name: string;
194 | message: string;
195 | };
196 | };
197 | }
198 |
199 | export interface OembedTweetV1Result {
200 | url: string;
201 | author_name: string;
202 | author_url: string;
203 | html: string;
204 | width: number;
205 | height: number | null;
206 | type: string;
207 | cache_age: string;
208 | provider_name: string;
209 | provider_url: string;
210 | version: string;
211 | }
212 |
213 | export interface TweetLookupMapV1Result {
214 | id: {
215 | [tweetId: string]: TweetV1 | null;
216 | };
217 | }
218 |
--------------------------------------------------------------------------------
/src/types/v2/community.v2.types.ts:
--------------------------------------------------------------------------------
1 | export interface CommunityV2 {
2 | id: string;
3 | name: string;
4 | created_at: string;
5 | }
6 |
7 | export interface CommunityErrorV2 {
8 | title: string;
9 | type: string;
10 | detail?: string;
11 | status?: number;
12 | }
13 |
14 | export interface CommunityV2Result {
15 | data: CommunityV2;
16 | errors?: CommunityErrorV2[];
17 | }
18 |
19 | export interface CommunitiesV2Result {
20 | data: CommunityV2[];
21 | errors?: CommunityErrorV2[];
22 | meta: {next_token?: string};
23 | }
24 |
25 | export interface CommunityByIDV2Params {
26 | id: string;
27 | }
28 |
29 | export interface CommunitySearchV2Params {
30 | query: string;
31 | max_results?: number;
32 | next_token?: string;
33 | pagination_token?: string;
34 | }
--------------------------------------------------------------------------------
/src/types/v2/dm.v2.types.ts:
--------------------------------------------------------------------------------
1 | import { TypeOrArrayOf } from '../shared.types';
2 | import { TTweetv2MediaField, TTweetv2TweetField, TTweetv2UserField } from './tweet.v2.types';
3 | import { ApiV2Includes, ReferencedTweetV2 } from './tweet.definition.v2';
4 | import { DataMetaAndIncludeV2, PaginableCountMetaV2 } from './shared.v2.types';
5 |
6 | export type TDMEventV2Field = 'id' | 'text' | 'event_type' | 'created_at' | 'dm_conversation_id' | 'sender_id' | 'participant_ids' | 'referenced_tweets' | 'attachments';
7 | export type TDMEventV2Expansion = 'attachments.media_keys' | 'referenced_tweets.id' | 'sender_id' | 'participant_ids';
8 | export type TDMEventV2EventType = 'MessageCreate' | 'ParticipantsJoin' | 'ParticipantsLeave';
9 |
10 | // GET /2/dm_events
11 |
12 | export interface GetDMEventV2Params {
13 | 'dm_event.fields': TypeOrArrayOf | string;
14 | event_types: TypeOrArrayOf | string;
15 | expansions: TypeOrArrayOf | string;
16 | max_results: number;
17 | 'media.fields': TypeOrArrayOf | string;
18 | pagination_token: string;
19 | 'tweet.fields': TypeOrArrayOf | string;
20 | 'user.fields': TypeOrArrayOf | string;
21 | }
22 |
23 | export type GetDMEventV2Result = DataMetaAndIncludeV2;
24 |
25 | // POST dm_conversations/:dm_conversation_id/messages
26 |
27 | export interface PostDMInConversationParams {
28 | attachments?: [{ media_id: string }];
29 | text?: string;
30 | }
31 |
32 | // POST dm_conversations
33 |
34 | export interface CreateDMConversationParams {
35 | conversation_type: 'Group';
36 | participant_ids: string[];
37 | message: PostDMInConversationParams;
38 | }
39 |
40 | export interface PostDMInConversationResult {
41 | dm_conversation_id: string;
42 | dm_event_id: string;
43 | }
44 |
45 | // Types
46 |
47 | export interface BaseDMEventV2 {
48 | id: string;
49 | created_at?: string;
50 | sender_id?: string;
51 | dm_conversation_id?: string;
52 | attachments?: DMEventAttachmentV2;
53 | referenced_tweets?: ReferencedTweetV2[];
54 | participant_ids?: string[];
55 | }
56 |
57 | export interface DMEventAttachmentV2 {
58 | media_keys: string[];
59 | }
60 |
61 | export type DMEventV2 = ({
62 | event_type: 'MessageCreate',
63 | text: string;
64 | } & BaseDMEventV2) | ({
65 | event_type: Extract
66 | } & BaseDMEventV2);
67 |
--------------------------------------------------------------------------------
/src/types/v2/index.ts:
--------------------------------------------------------------------------------
1 | export * from './streaming.v2.types';
2 | export * from './tweet.v2.types';
3 | export * from './tweet.definition.v2';
4 | export * from './user.v2.types';
5 | export * from './spaces.v2.types';
6 | export * from './list.v2.types';
7 | export * from './community.v2.types';
8 |
--------------------------------------------------------------------------------
/src/types/v2/list.v2.types.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOrArrayOf } from '../shared.types';
2 | import type { DataAndIncludeV2, DataMetaAndIncludeV2, DataV2, PaginableCountMetaV2 } from './shared.v2.types';
3 | import type { TTweetv2UserField } from './tweet.v2.types';
4 | import type { UserV2 } from './user.v2.types';
5 |
6 | export type TListV2Field = 'created_at' | 'follower_count' | 'member_count' | 'private' | 'description' | 'owner_id';
7 | export type TListV2Expansion = 'owner_id';
8 | export type TListV2Includes = { users?: UserV2[] };
9 |
10 | export interface ListV2 {
11 | id: string;
12 | name: string;
13 | created_at?: string;
14 | private?: boolean;
15 | follower_count?: number;
16 | member_count?: number;
17 | owner_id?: string;
18 | description?: string;
19 | }
20 |
21 | export interface ListCreateV2Params {
22 | name: string;
23 | description?: string;
24 | private?: boolean;
25 | }
26 |
27 | export interface GetListV2Params {
28 | expansions: TypeOrArrayOf | string;
29 | 'list.fields': TypeOrArrayOf | string;
30 | 'user.fields': TypeOrArrayOf | string;
31 | }
32 |
33 | export interface GetListTimelineV2Params extends Partial {
34 | max_results?: number;
35 | pagination_token?: string;
36 | }
37 |
38 | export type ListGetV2Result = DataAndIncludeV2;
39 |
40 | export type ListTimelineV2Result = DataMetaAndIncludeV2
41 |
42 | export type ListCreateV2Result = DataV2<{ id: string, name: string }>;
43 |
44 | export type ListUpdateV2Params = Omit & { name?: string };
45 |
46 | export type ListUpdateV2Result = DataV2<{ updated: true }>;
47 |
48 | export type ListDeleteV2Result = DataV2<{ deleted: true }>;
49 |
50 | export type ListMemberV2Result = DataV2<{ is_member: boolean }>;
51 |
52 | export type ListFollowV2Result = DataV2<{ following: boolean }>;
53 |
54 | export type ListPinV2Result = DataV2<{ pinned: boolean }>;
55 |
--------------------------------------------------------------------------------
/src/types/v2/media.v2.types.ts:
--------------------------------------------------------------------------------
1 | export type MediaV2MediaCategory = 'tweet_image' | 'tweet_video' | 'tweet_gif' | 'dm_image' | 'dm_video' | 'dm_gif' | 'subtitles';
2 |
3 | export interface MediaV2UploadInitParams {
4 | additional_owners?: string[];
5 | media_category?: MediaV2MediaCategory;
6 | media_type: string;
7 | total_bytes: number;
8 | }
9 |
10 | export interface MediaV2UploadAppendParams {
11 | segment_index: number;
12 | media: Buffer;
13 | }
14 |
15 | export interface MediaV2ProcessingInfo {
16 | state: 'pending' | 'in_progress' | 'failed' | 'succeeded';
17 | check_after_secs?: number;
18 | error?: {
19 | code: number;
20 | message: string;
21 | };
22 | }
23 |
24 | export interface MediaV2UploadResponse {
25 | data: {
26 | id: string;
27 | media_key: string;
28 | size?: number;
29 | expires_after_secs: number;
30 | processing_info?: MediaV2ProcessingInfo;
31 | };
32 | }
33 |
34 | export interface MediaV2MetadataCreateParams {
35 | alt_text?: { text: string };
36 | }
37 |
38 | export interface MediaV2MetadataCreateResult {
39 | data: {
40 | id: string;
41 | associated_metadata: {
42 | alt_text: { text: string };
43 | };
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/types/v2/shared.v2.types.ts:
--------------------------------------------------------------------------------
1 | import type { InlineErrorV2 } from '../errors.types';
2 |
3 | export type MetaV2 = { meta: M, errors?: InlineErrorV2[] };
4 | export type DataV2 = { data: D, errors?: InlineErrorV2[] };
5 | export type IncludeV2 = { includes?: I, errors?: InlineErrorV2[] };
6 |
7 | export type DataAndMetaV2 = { data: D, meta: M, errors?: InlineErrorV2[] };
8 | export type DataAndIncludeV2 = { data: D, includes?: I, errors?: InlineErrorV2[] };
9 | export type DataMetaAndIncludeV2 = { data: D, meta: M, includes?: I, errors?: InlineErrorV2[] };
10 |
11 | export interface SentMeta {
12 | /** The time when the request body was returned. */
13 | sent: string;
14 | }
15 |
16 | export interface PaginableCountMetaV2 {
17 | result_count: number;
18 | next_token?: string;
19 | previous_token?: string;
20 | }
21 |
--------------------------------------------------------------------------------
/src/types/v2/spaces.v2.types.ts:
--------------------------------------------------------------------------------
1 | import type { TypeOrArrayOf } from '../shared.types';
2 | import type { DataAndIncludeV2, DataMetaAndIncludeV2 } from './shared.v2.types';
3 | import type { TTweetv2UserField } from './tweet.v2.types';
4 | import type { UsersV2Params, UsersV2Result, UserV2 } from './user.v2.types';
5 |
6 | export interface SpaceV2FieldsParams {
7 | expansions: TypeOrArrayOf | string;
8 | 'space.fields': TypeOrArrayOf | string;
9 | 'user.fields': TypeOrArrayOf | string;
10 | }
11 |
12 | export type TSpaceV2Expansion = 'invited_user_ids' | 'speaker_ids' | 'creator_id' | 'host_ids';
13 | export type TSpaceV2SpaceField = 'host_ids' | 'created_at' | 'creator_id' | 'id' | 'lang'
14 | | 'invited_user_ids' | 'participant_count' | 'speaker_ids' | 'started_at' | 'state' | 'title'
15 | | 'updated_at' | 'scheduled_start' | 'is_ticketed' | 'topic_ids' | 'ended_at' | 'subscriber_count';
16 | export type TSpaceV2State = 'canceled' | 'ended' | 'live' | 'scheduled';
17 |
18 | // - Requests -
19 |
20 | export interface SpaceV2CreatorLookupParams extends SpaceV2FieldsParams {
21 | max_results?: number;
22 | }
23 |
24 | export interface SpaceV2SearchParams extends Partial {
25 | query: string;
26 | state: TSpaceV2State;
27 | max_results?: number;
28 | }
29 |
30 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
31 | export interface SpaceV2BuyersParams extends Partial {}
32 |
33 | // - Responses -
34 |
35 | type SpaceV2Includes = { users?: UserV2[] };
36 |
37 | export type SpaceV2SingleResult = DataAndIncludeV2;
38 | export type SpaceV2LookupResult = DataMetaAndIncludeV2;
39 | export type SpaceV2BuyersResult = UsersV2Result;
40 |
41 | // - Entities -
42 |
43 | export interface SpaceV2 {
44 | id: string;
45 | state: TSpaceV2State;
46 | created_at?: string;
47 | host_ids?: string[];
48 | lang?: string;
49 | is_ticketed?: boolean;
50 | invited_user_ids?: string[];
51 | participant_count?: number;
52 | scheduled_start?: string;
53 | speaker_ids?: string[];
54 | started_at?: string;
55 | title?: string;
56 | creator_id?: string;
57 | updated_at?: string;
58 | topic_ids?: string[];
59 | ended_at?: string;
60 | subscriber_count?: number;
61 | }
62 |
--------------------------------------------------------------------------------
/src/types/v2/streaming.v2.types.ts:
--------------------------------------------------------------------------------
1 | // ---------------
2 | // -- Streaming --
3 | // ---------------
4 |
5 | // -- Get stream rules --
6 |
7 | import { DataAndMetaV2, MetaV2, SentMeta } from './shared.v2.types';
8 |
9 | export interface StreamingV2GetRulesParams {
10 | /** Comma-separated list of rule IDs. If omitted, all rules are returned. */
11 | ids: string;
12 | }
13 |
14 | export interface StreamingV2Rule {
15 | /** Unique identifier of this rule. */
16 | id: string;
17 | /** The rule text as submitted when creating the rule. */
18 | value: string;
19 | /** The tag label as defined when creating the rule. */
20 | tag?: string;
21 | }
22 |
23 | export type StreamingV2GetRulesResult = DataAndMetaV2;
24 |
25 | // -- Add / delete stream rules --
26 |
27 | export interface StreamingV2AddRulesParams {
28 | /** Specifies the operation you want to perform on the rules. */
29 | add: {
30 | /**
31 | * The tag label.
32 | * This is a free-form text you can use to identify the rules that matched a specific Tweet in the streaming response.
33 | * Tags can be the same across rules.
34 | */
35 | tag?: string;
36 | /**
37 | * The rule text.
38 | * If you are using a Standard Project at the Basic access level,
39 | * you can use the basic set of operators, can submit up to 25 concurrent rules, and can submit rules up to 512 characters long.
40 | * If you are using an Academic Research Project at the Basic access level,
41 | * you can use all available operators, can submit up to 1,000 concurrent rules, and can submit rules up to 1,024 characters long.
42 | */
43 | value: string;
44 | }[];
45 | }
46 |
47 | export interface StreamingV2DeleteRulesParams {
48 | /** Specifies the operation you want to perform on the rules. */
49 | delete: {
50 | /** Array of rule IDs, each one representing a rule already active in your stream. IDs must be submitted as strings. */
51 | ids: string[];
52 | };
53 | }
54 |
55 | export type StreamingV2UpdateRulesParams = StreamingV2AddRulesParams | StreamingV2DeleteRulesParams;
56 |
57 | export interface StreamingV2UpdateRulesQuery {
58 | /**
59 | * Set to true to test a the syntax of your rule without submitting it.
60 | * This is useful if you want to check the syntax of a rule before removing one or more of your existing rules.
61 | */
62 | dry_run: boolean;
63 | }
64 |
65 | export type StreamingV2UpdateRulesAddResult = DataAndMetaV2;
73 |
74 | export type StreamingV2UpdateRulesDeleteResult = MetaV2;
80 |
81 | export type StreamingV2UpdateRulesResult = StreamingV2UpdateRulesAddResult | StreamingV2UpdateRulesDeleteResult;
82 |
--------------------------------------------------------------------------------
/src/types/v2/tweet.definition.v2.ts:
--------------------------------------------------------------------------------
1 | import type { UserV2 } from './user.v2.types';
2 |
3 | export interface PlaceV2 {
4 | full_name: string;
5 | id: string;
6 | contained_within?: string[];
7 | country?: string;
8 | country_code?: string;
9 | geo?: {
10 | type: string;
11 | bbox: number[];
12 | properties: any;
13 | };
14 | name?: string;
15 | place_type?: string;
16 | }
17 |
18 | export interface PlaybackCountV2 {
19 | playback_0_count: number;
20 | playback_25_count: number;
21 | playback_50_count: number;
22 | playback_75_count: number;
23 | playback_100_count: number;
24 | }
25 |
26 | export type OrganicMetricV2 = PlaybackCountV2 & { view_count: number };
27 |
28 | export interface MediaVariantsV2 {
29 | bit_rate?: number;
30 | content_type: 'video/mp4' | 'application/x-mpegURL' | string;
31 | url: string
32 | }
33 |
34 | export interface MediaObjectV2 {
35 | media_key: string;
36 | type: 'video' | 'animated_gif' | 'photo' | string;
37 | duration_ms?: number;
38 | height?: number;
39 | width?: number;
40 | url?: string;
41 | preview_image_url?: string;
42 | alt_text?: string;
43 | non_public_metrics?: PlaybackCountV2;
44 | organic_metrics?: OrganicMetricV2;
45 | promoted_metrics?: OrganicMetricV2;
46 | public_metrics?: { view_count: number };
47 | variants?: MediaVariantsV2[];
48 | }
49 |
50 | export interface PollV2 {
51 | id: string;
52 | options: { position: number; label: string; votes: number; }[];
53 | duration_minutes?: number;
54 | end_datetime?: string;
55 | voting_status?: string;
56 | }
57 |
58 | export interface ReferencedTweetV2 {
59 | type: 'retweeted' | 'quoted' | 'replied_to';
60 | id: string;
61 | }
62 |
63 | export interface TweetAttachmentV2 {
64 | media_keys?: string[];
65 | poll_ids?: string[];
66 | }
67 |
68 | export interface TweetGeoV2 {
69 | coordinates: {
70 | type: string;
71 | coordinates: [number, number] | null;
72 | };
73 | place_id: string;
74 | }
75 |
76 | interface TweetContextAnnotationItemV2 {
77 | id: string;
78 | name: string;
79 | description?: string;
80 | }
81 |
82 | export type TweetContextAnnotationDomainV2 = TweetContextAnnotationItemV2;
83 | export type TweetContextAnnotationEntityV2 = TweetContextAnnotationItemV2;
84 |
85 | export interface TweetContextAnnotationV2 {
86 | domain: TweetContextAnnotationDomainV2;
87 | entity: TweetContextAnnotationEntityV2;
88 | }
89 |
90 | export interface TweetEntityAnnotationsV2 {
91 | start: number;
92 | end: number;
93 | probability: number;
94 | type: string;
95 | normalized_text: string;
96 | }
97 |
98 | export interface TweetEntityUrlV2 {
99 | start: number;
100 | end: number;
101 | url: string;
102 | expanded_url: string;
103 | display_url: string;
104 | unwound_url: string;
105 | title?: string;
106 | description?: string;
107 | status?: string;
108 | images?: TweetEntityUrlImageV2[];
109 | media_key?: string;
110 | }
111 |
112 | export interface TweetEntityUrlImageV2 {
113 | url: string;
114 | width: number;
115 | height: number;
116 | }
117 |
118 | export interface TweetEntityHashtagV2 {
119 | start: number;
120 | end: number;
121 | tag: string;
122 | }
123 |
124 | export interface TweetEntityMentionV2 {
125 | start: number;
126 | end: number;
127 | username: string;
128 | id: string;
129 | }
130 |
131 | export interface TweetEntitiesV2 {
132 | annotations: TweetEntityAnnotationsV2[];
133 | urls: TweetEntityUrlV2[];
134 | hashtags: TweetEntityHashtagV2[];
135 | cashtags: TweetEntityHashtagV2[];
136 | mentions: TweetEntityMentionV2[];
137 | }
138 |
139 | export interface TweetWithheldInfoV2 {
140 | copyright: boolean;
141 | country_codes: string[];
142 | scope: 'tweet' | 'user';
143 | }
144 |
145 | export interface TweetPublicMetricsV2 {
146 | retweet_count: number;
147 | reply_count: number;
148 | like_count: number;
149 | quote_count: number;
150 | impression_count: number;
151 | bookmark_count?: number;
152 | }
153 |
154 | export interface TweetNonPublicMetricsV2 {
155 | impression_count: number;
156 | url_link_clicks: number;
157 | user_profile_clicks: number;
158 | }
159 |
160 | export interface TweetOrganicMetricsV2 {
161 | impression_count: number;
162 | url_link_clicks: number;
163 | user_profile_clicks: number;
164 | retweet_count: number;
165 | reply_count: number;
166 | like_count: number;
167 | }
168 |
169 | export type TweetPromotedMetricsV2 = TweetOrganicMetricsV2;
170 |
171 | export interface NoteTweetV2 {
172 | text: string;
173 | entities?: NoteTweetEntitiesV2;
174 | }
175 |
176 | export type NoteTweetEntitiesV2 = Omit;
177 |
178 | export type TTweetReplySettingsV2 = 'mentionedUsers' | 'following' | 'everyone';
179 |
180 | export interface SendTweetV2Params {
181 | direct_message_deep_link?: string;
182 | for_super_followers_only?: 'True' | 'False';
183 | geo?: {
184 | place_id: string;
185 | };
186 | media?: {
187 | media_ids?:
188 | | [string]
189 | | [string, string]
190 | | [string, string, string]
191 | | [string, string, string, string];
192 | tagged_user_ids?: string[];
193 | };
194 | poll?: {
195 | duration_minutes: number;
196 | options: string[];
197 | };
198 | quote_tweet_id?: string;
199 | reply?: {
200 | exclude_reply_user_ids?: string[];
201 | in_reply_to_tweet_id: string;
202 | };
203 | reply_settings?: TTweetReplySettingsV2 | string;
204 | text?: string;
205 | community_id?: string;
206 | }
207 |
208 | //// FINALLY, TweetV2
209 | export interface TweetV2 {
210 | id: string;
211 | text: string;
212 | edit_history_tweet_ids: string[];
213 | created_at?: string;
214 | author_id?: string;
215 | conversation_id?: string;
216 | in_reply_to_user_id?: string;
217 | referenced_tweets?: ReferencedTweetV2[];
218 | attachments?: TweetAttachmentV2;
219 | geo?: TweetGeoV2;
220 | context_annotations?: TweetContextAnnotationV2[];
221 | entities?: TweetEntitiesV2;
222 | withheld?: TweetWithheldInfoV2;
223 | public_metrics?: TweetPublicMetricsV2;
224 | non_public_metrics?: TweetNonPublicMetricsV2;
225 | organic_metrics?: TweetOrganicMetricsV2;
226 | promoted_metrics?: TweetPromotedMetricsV2;
227 | possibly_sensitive?: boolean;
228 | lang?: string;
229 | reply_settings?: 'everyone' | 'mentionedUsers' | 'following';
230 | source?: string;
231 | note_tweet?: NoteTweetV2;
232 | community_id?: string;
233 | }
234 |
235 | export interface ApiV2Includes {
236 | tweets?: TweetV2[];
237 | users?: UserV2[];
238 | places?: PlaceV2[];
239 | media?: MediaObjectV2[];
240 | polls?: PollV2[];
241 | }
242 |
--------------------------------------------------------------------------------
/src/types/v2/user.v2.types.ts:
--------------------------------------------------------------------------------
1 | // Users
2 | import type { CashtagEntity, HashtagEntity, MentionEntity, UrlEntity } from '../entities.types';
3 | import type { ApiV2Includes } from './tweet.definition.v2';
4 | import type { DataAndIncludeV2, DataMetaAndIncludeV2, DataV2 } from './shared.v2.types';
5 | import type { TTweetv2MediaField, TTweetv2PlaceField, TTweetv2PollField, TTweetv2TweetField, TTweetv2UserField } from './tweet.v2.types';
6 | import type { TypeOrArrayOf } from '../shared.types';
7 | import { PaginableCountMetaV2 } from './shared.v2.types';
8 |
9 | export type TUserV2Expansion = 'pinned_tweet_id';
10 |
11 | // - Params -
12 |
13 | export interface UsersV2Params {
14 | expansions: TypeOrArrayOf;
15 | 'media.fields': TypeOrArrayOf | string;
16 | 'place.fields': TypeOrArrayOf | string;
17 | 'poll.fields': TypeOrArrayOf | string;
18 | 'tweet.fields': TypeOrArrayOf | string;
19 | 'user.fields': TypeOrArrayOf | string;
20 | }
21 |
22 | export interface UserV2TimelineParams {
23 | expansions?: TypeOrArrayOf;
24 | 'media.fields'?: TypeOrArrayOf | string;
25 | 'place.fields'?: TypeOrArrayOf | string;
26 | 'poll.fields'?: TypeOrArrayOf | string;
27 | 'tweet.fields'?: TypeOrArrayOf | string;
28 | 'user.fields'?: TypeOrArrayOf | string;
29 | max_results?: number;
30 | pagination_token?: string;
31 | }
32 |
33 | export interface TweetRetweetedOrLikedByV2Params extends UserV2TimelineParams {
34 | asPaginator?: boolean;
35 | }
36 |
37 | export interface TweetRetweetedOrLikedByV2ParamsWithoutPaginator extends TweetRetweetedOrLikedByV2Params {
38 | asPaginator?: false;
39 | }
40 |
41 | export interface TweetRetweetedOrLikedByV2ParamsWithPaginator extends TweetRetweetedOrLikedByV2Params {
42 | asPaginator: true;
43 | }
44 |
45 | export interface FollowersV2Params extends UserV2TimelineParams {
46 | asPaginator?: boolean;
47 | }
48 |
49 | export interface FollowersV2ParamsWithoutPaginator extends FollowersV2Params {
50 | asPaginator?: false;
51 | }
52 |
53 | // Polymorphism for { asPaginator: true } prop
54 | export interface FollowersV2ParamsWithPaginator extends FollowersV2Params {
55 | asPaginator: true;
56 | }
57 |
58 | // - Results -
59 |
60 | export type UserV2Result = DataAndIncludeV2;
61 | export type UsersV2Result = DataAndIncludeV2;
62 |
63 | export type UserV2FollowResult = DataV2<{
64 | following: boolean;
65 | pending_follow: boolean;
66 | }>;
67 |
68 | export type UserV2UnfollowResult = DataV2<{
69 | following: boolean;
70 | }>;
71 |
72 | export type UserV2BlockResult = DataV2<{
73 | blocking: boolean;
74 | }>;
75 |
76 | export type UserV2MuteResult = DataV2<{
77 | muting: boolean;
78 | }>;
79 |
80 | export type UserV2TimelineResult = DataMetaAndIncludeV2;
81 |
82 | /** @deprecated Use {UserV2TimelineResult} instead. */
83 | export type FollowersV2Result = UserV2TimelineResult;
84 |
85 | // - Entities -
86 |
87 | export interface UserV2 {
88 | id: string;
89 | name: string;
90 | username: string;
91 | created_at?: string; // ISO 8601 date
92 | protected?: boolean;
93 | withheld?: {
94 | country_codes?: string[];
95 | scope?: 'user';
96 | }
97 | location?: string;
98 | url?: string;
99 | description?: string;
100 | verified?: boolean;
101 | verified_type?: 'none' | 'blue' | 'business' | 'government';
102 | entities?: {
103 | url?: { urls: UrlEntity[] };
104 | description: {
105 | urls?: UrlEntity[];
106 | hashtags?: HashtagEntity[];
107 | cashtags?: CashtagEntity[];
108 | mentions?: MentionEntity[];
109 | }
110 | }
111 | profile_image_url?: string;
112 | profile_banner_url?: string;
113 | public_metrics?: {
114 | followers_count?: number;
115 | following_count?: number;
116 | tweet_count?: number;
117 | listed_count?: number;
118 | like_count?: number;
119 | media_count?: number;
120 | }
121 | pinned_tweet_id?: string;
122 | connection_status?: string[];
123 | most_recent_tweet_id?: string;
124 | }
125 |
--------------------------------------------------------------------------------
/src/v1/media-helpers.v1.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { safeDeprecationWarning } from '../helpers';
3 | import type { TUploadableMedia, TUploadTypeV1 } from '../types';
4 | import { EUploadMimeType } from '../types';
5 |
6 | // -------------
7 | // Media helpers
8 | // -------------
9 |
10 | export type TFileHandle = fs.promises.FileHandle | number | Buffer;
11 |
12 | export async function readFileIntoBuffer(file: TUploadableMedia) {
13 | const handle = await getFileHandle(file);
14 |
15 | if (typeof handle === 'number') {
16 | return new Promise((resolve, reject) => {
17 | fs.readFile(handle, (err, data) => {
18 | if (err) {
19 | return reject(err);
20 | }
21 | resolve(data);
22 | });
23 | });
24 | } else if (handle instanceof Buffer) {
25 | return handle;
26 | } else {
27 | return handle.readFile();
28 | }
29 | }
30 |
31 | export function getFileHandle(file: TUploadableMedia) {
32 | if (typeof file === 'string') {
33 | return fs.promises.open(file, 'r');
34 | } else if (typeof file === 'number') {
35 | return file;
36 | } else if (typeof file === 'object' && !(file instanceof Buffer)) {
37 | return file;
38 | } else if (!(file instanceof Buffer)) {
39 | throw new Error('Given file is not valid, please check its type.');
40 | } else {
41 | return file;
42 | }
43 | }
44 |
45 | export async function getFileSizeFromFileHandle(fileHandle: TFileHandle) {
46 | // Get the file size
47 | if (typeof fileHandle === 'number') {
48 | const stats = await new Promise((resolve, reject) => {
49 | fs.fstat(fileHandle as number, (err, stats) => {
50 | if (err) reject(err);
51 | resolve(stats);
52 | });
53 | }) as fs.Stats;
54 |
55 | return stats.size;
56 | } else if (fileHandle instanceof Buffer) {
57 | return fileHandle.length;
58 | } else {
59 | return (await fileHandle.stat()).size;
60 | }
61 | }
62 |
63 | export function getMimeType(file: TUploadableMedia, type?: TUploadTypeV1 | string, mimeType?: EUploadMimeType | string) {
64 | if (typeof mimeType === 'string') {
65 | return mimeType;
66 | } else if (typeof file === 'string' && !type) {
67 | return getMimeByName(file);
68 | } else if (typeof type === 'string') {
69 | return getMimeByType(type);
70 | }
71 |
72 | throw new Error('You must specify type if file is a file handle or Buffer.');
73 | }
74 |
75 | function getMimeByName(name: string) {
76 | if (name.endsWith('.jpeg') || name.endsWith('.jpg')) return EUploadMimeType.Jpeg;
77 | if (name.endsWith('.png')) return EUploadMimeType.Png;
78 | if (name.endsWith('.webp')) return EUploadMimeType.Webp;
79 | if (name.endsWith('.gif')) return EUploadMimeType.Gif;
80 | if (name.endsWith('.mpeg4') || name.endsWith('.mp4')) return EUploadMimeType.Mp4;
81 | if (name.endsWith('.mov') || name.endsWith('.mov')) return EUploadMimeType.Mov;
82 | if (name.endsWith('.srt')) return EUploadMimeType.Srt;
83 |
84 | safeDeprecationWarning({
85 | instance: 'TwitterApiv1ReadWrite',
86 | method: 'uploadMedia',
87 | problem: 'options.mimeType is missing and filename couldn\'t help to resolve MIME type, so it will fallback to image/jpeg',
88 | resolution: 'If you except to give filenames without extensions, please specify explicitlty the MIME type using options.mimeType',
89 | });
90 |
91 | return EUploadMimeType.Jpeg;
92 | }
93 |
94 | function getMimeByType(type: TUploadTypeV1 | string) {
95 | safeDeprecationWarning({
96 | instance: 'TwitterApiv1ReadWrite',
97 | method: 'uploadMedia',
98 | problem: 'you\'re using options.type',
99 | resolution: 'Remove options.type argument and migrate to options.mimeType which takes the real MIME type. ' +
100 | 'If you\'re using type=longmp4, add options.longVideo alongside of mimeType=EUploadMimeType.Mp4',
101 | });
102 |
103 | if (type === 'gif') return EUploadMimeType.Gif;
104 | if (type === 'jpg') return EUploadMimeType.Jpeg;
105 | if (type === 'png') return EUploadMimeType.Png;
106 | if (type === 'webp') return EUploadMimeType.Webp;
107 | if (type === 'srt') return EUploadMimeType.Srt;
108 | if (type === 'mp4' || type === 'longmp4') return EUploadMimeType.Mp4;
109 | if (type === 'mov') return EUploadMimeType.Mov;
110 |
111 | return type;
112 | }
113 |
114 | export function getMediaCategoryByMime(name: string, target: 'tweet' | 'dm') {
115 | if (name === EUploadMimeType.Mp4 || name === EUploadMimeType.Mov) return target === 'tweet' ? 'TweetVideo' : 'DmVideo';
116 | if (name === EUploadMimeType.Gif) return target === 'tweet' ? 'TweetGif' : 'DmGif';
117 | if (name === EUploadMimeType.Srt) return 'Subtitles';
118 | else return target === 'tweet' ? 'TweetImage' : 'DmImage';
119 | }
120 |
121 | export function sleepSecs(seconds: number) {
122 | return new Promise(resolve => setTimeout(resolve, seconds * 1000));
123 | }
124 |
125 | export async function readNextPartOf(file: TFileHandle, chunkLength: number, bufferOffset = 0, buffer?: Buffer): Promise<[Buffer, number]> {
126 | if (file instanceof Buffer) {
127 | const rt = file.slice(bufferOffset, bufferOffset + chunkLength);
128 | return [rt, rt.length];
129 | }
130 |
131 | if (!buffer) {
132 | throw new Error('Well, we will need a buffer to store file content.');
133 | }
134 |
135 | let bytesRead: number;
136 |
137 | if (typeof file === 'number') {
138 | bytesRead = await new Promise((resolve, reject) => {
139 | fs.read(file as number, buffer, 0, chunkLength, bufferOffset, (err, nread) => {
140 | if (err) reject(err);
141 | resolve(nread);
142 | });
143 | });
144 | }
145 | else {
146 | const res = await file.read(buffer, 0, chunkLength, bufferOffset);
147 | bytesRead = res.bytesRead;
148 | }
149 |
150 | return [buffer, bytesRead];
151 | }
152 |
--------------------------------------------------------------------------------
/src/v2-labs/client.v2.labs.read.ts:
--------------------------------------------------------------------------------
1 | import TwitterApiSubClient from '../client.subclient';
2 | import { API_V2_LABS_PREFIX } from '../globals';
3 |
4 | /**
5 | * Base Twitter v2 labs client with only read right.
6 | */
7 | export default class TwitterApiv2LabsReadOnly extends TwitterApiSubClient {
8 | protected _prefix = API_V2_LABS_PREFIX;
9 | }
10 |
--------------------------------------------------------------------------------
/src/v2-labs/client.v2.labs.ts:
--------------------------------------------------------------------------------
1 | import { API_V2_LABS_PREFIX } from '../globals';
2 | import TwitterApiv2LabsReadWrite from './client.v2.labs.write';
3 |
4 | /**
5 | * Twitter v2 labs client with all rights (read/write/DMs)
6 | */
7 | export class TwitterApiv2Labs extends TwitterApiv2LabsReadWrite {
8 | protected _prefix = API_V2_LABS_PREFIX;
9 |
10 | /**
11 | * Get a client with read/write rights.
12 | */
13 | public get readWrite() {
14 | return this as TwitterApiv2LabsReadWrite;
15 | }
16 | }
17 |
18 | export default TwitterApiv2Labs;
19 |
--------------------------------------------------------------------------------
/src/v2-labs/client.v2.labs.write.ts:
--------------------------------------------------------------------------------
1 | import { API_V2_LABS_PREFIX } from '../globals';
2 | import TwitterApiv2LabsReadOnly from './client.v2.labs.read';
3 |
4 | /**
5 | * Base Twitter v2 labs client with read/write rights.
6 | */
7 | export default class TwitterApiv2LabsReadWrite extends TwitterApiv2LabsReadOnly {
8 | protected _prefix = API_V2_LABS_PREFIX;
9 |
10 | /**
11 | * Get a client with only read rights.
12 | */
13 | public get readOnly() {
14 | return this as TwitterApiv2LabsReadOnly;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/v2/client.v2.ts:
--------------------------------------------------------------------------------
1 | import { API_V2_PREFIX } from '../globals';
2 | import TwitterApiv2ReadWrite from './client.v2.write';
3 | import TwitterApiv2Labs from '../v2-labs/client.v2.labs';
4 |
5 | /**
6 | * Twitter v2 client with all rights (read/write/DMs)
7 | */
8 | export class TwitterApiv2 extends TwitterApiv2ReadWrite {
9 | protected _prefix = API_V2_PREFIX;
10 | protected _labs?: TwitterApiv2Labs;
11 |
12 | /* Sub-clients */
13 |
14 | /**
15 | * Get a client with read/write rights.
16 | */
17 | public get readWrite() {
18 | return this as TwitterApiv2ReadWrite;
19 | }
20 |
21 | /**
22 | * Get a client for v2 labs endpoints.
23 | */
24 | public get labs() {
25 | if (this._labs) return this._labs;
26 |
27 | return this._labs = new TwitterApiv2Labs(this);
28 | }
29 |
30 | /** API endpoints */
31 |
32 |
33 | }
34 |
35 | export default TwitterApiv2;
36 |
--------------------------------------------------------------------------------
/test/account.v1.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 |
8 | describe('Account endpoints for v1.1 API', () => {
9 | before(() => {
10 | client = getUserClient();
11 | });
12 |
13 | it('.accountSettings/.updateAccountSettings/.updateAccountProfile - Change account settings & profile', async () => {
14 | const user = await client.currentUser();
15 | const settings = await client.v1.accountSettings();
16 |
17 | expect(settings.language).to.be.a('string');
18 |
19 | const testBio = 'Hello, test bio ' + String(Math.random());
20 | await client.v1.updateAccountProfile({ description: testBio });
21 |
22 | const modifiedUser = await client.currentUser(true);
23 | expect(modifiedUser.description).to.equal(testBio);
24 |
25 | await client.v1.updateAccountProfile({ description: user.description as string });
26 |
27 | await client.v1.updateAccountSettings({ lang: 'en' });
28 | const updatedSettings = await client.v1.accountSettings();
29 | expect(updatedSettings.language).to.eq('en');
30 |
31 | await client.v1.updateAccountSettings({ lang: settings.language });
32 | }).timeout(60 * 1000);
33 | });
34 |
--------------------------------------------------------------------------------
/test/assets/bbb.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PLhery/node-twitter-api-v2/1defe1a5d046cff0b7b49ff217ab17273079b37b/test/assets/bbb.mp4
--------------------------------------------------------------------------------
/test/assets/lolo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PLhery/node-twitter-api-v2/1defe1a5d046cff0b7b49ff217ab17273079b37b/test/assets/lolo.jpg
--------------------------------------------------------------------------------
/test/assets/pec.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PLhery/node-twitter-api-v2/1defe1a5d046cff0b7b49ff217ab17273079b37b/test/assets/pec.gif
--------------------------------------------------------------------------------
/test/auth.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { getRequestClient } from '../src/test/utils';
4 |
5 | // OAuth 1.0a
6 | const clientWithoutUser = getRequestClient();
7 |
8 | describe('Authentication API', () => {
9 | it('.generateAuthLink - Create a auth link', async () => {
10 | const tokens = await clientWithoutUser.generateAuthLink('oob');
11 |
12 | expect(tokens.oauth_token).to.be.a('string');
13 | expect(tokens.oauth_token_secret).to.be.a('string');
14 | expect(tokens.oauth_callback_confirmed).to.be.equal('true');
15 | expect(tokens.url).to.be.a('string');
16 | }).timeout(1000 * 120);
17 | });
18 |
--------------------------------------------------------------------------------
/test/dm.v1.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { EDirectMessageEventTypeV1, ReceivedWelcomeDMCreateEventV1, TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 | import { sleepSecs } from '../src/v1/media-helpers.v1';
6 |
7 | let client: TwitterApi;
8 |
9 | const TARGET_USER_ID = process.env.TARGET_DM_USER_ID as string;
10 | const TEST_UUID = Math.random();
11 | let isDmTestEnabled = false;
12 |
13 | if (process.env.TARGET_DM_USER_ID) {
14 | isDmTestEnabled = true;
15 | }
16 |
17 | describe.skip('DM endpoints for v1.1 API', () => {
18 | before(() => {
19 | client = getUserClient();
20 | });
21 |
22 | it('.sendDm/.getDmEvent/.deleteDm - Send a new direct message and fetch it.', async () => {
23 | if (!isDmTestEnabled) {
24 | return;
25 | }
26 |
27 | const v1Client = client.v1;
28 | const selfAccount = await client.currentUser();
29 | const msgText = `Hello, this is a new direct message from an automated script - UUID: ${TEST_UUID}`;
30 |
31 | const sentMessage = await v1Client.sendDm({
32 | recipient_id: TARGET_USER_ID,
33 | text: msgText,
34 | });
35 |
36 | expect(sentMessage.event.type).to.equal(EDirectMessageEventTypeV1.Create);
37 |
38 | // Get event to repeat action
39 | const messageEvent = await v1Client.getDmEvent(sentMessage.event.id);
40 |
41 | for (const evt of [sentMessage.event, messageEvent.event]) {
42 | const msgCreate = evt[EDirectMessageEventTypeV1.Create];
43 | expect(msgCreate).to.be.ok;
44 | expect(msgCreate).to.haveOwnProperty('message_data');
45 | expect(msgCreate.message_data.text).to.equal(msgText);
46 | expect(msgCreate.message_data.quick_reply).to.be.undefined;
47 | expect(msgCreate.message_data.quick_reply_response).to.be.undefined;
48 | expect(msgCreate.message_data.attachment).to.be.undefined;
49 | expect(msgCreate.message_data.ctas).to.be.undefined;
50 | expect(msgCreate.target.recipient_id).to.equal(TARGET_USER_ID);
51 | expect(msgCreate.sender_id).to.equal(selfAccount.id_str);
52 | }
53 | }).timeout(60 * 1000);
54 |
55 | it('.listDmEvents/.deleteDm - List DM events and delete every available DM sent to TARGET USER ID.', async () => {
56 | if (!isDmTestEnabled) {
57 | return;
58 | }
59 |
60 | const v1Client = client.v1;
61 | const selfAccount = await client.currentUser();
62 |
63 | const eventPaginator = await v1Client.listDmEvents();
64 |
65 | for await (const evt of eventPaginator) {
66 | expect(evt.type).to.equal(EDirectMessageEventTypeV1.Create);
67 |
68 | const msgCreate = evt[EDirectMessageEventTypeV1.Create];
69 | expect(msgCreate).to.be.ok;
70 |
71 | if (msgCreate.target.recipient_id !== TARGET_USER_ID) {
72 | continue;
73 | }
74 |
75 | expect(msgCreate).to.haveOwnProperty('message_data');
76 | expect(msgCreate.message_data.quick_reply).to.be.undefined;
77 | expect(msgCreate.message_data.quick_reply_response).to.be.undefined;
78 | expect(msgCreate.message_data.attachment).to.be.undefined;
79 | expect(msgCreate.message_data.ctas).to.be.undefined;
80 | expect(msgCreate.target.recipient_id).to.equal(TARGET_USER_ID);
81 | expect(msgCreate.sender_id).to.equal(selfAccount.id_str);
82 |
83 | await v1Client.deleteDm(evt.id);
84 | }
85 | }).timeout(60 * 1000);
86 |
87 | it('.newWelcomeDm/.getWelcomeDm/.updateWelcomeDm/.listWelcomeDms/.deleteWelcomeDm - Every method related to welcome messages.', async () => {
88 | if (!isDmTestEnabled) {
89 | return;
90 | }
91 |
92 | const v1Client = client.v1;
93 |
94 | // NEW WELCOME MESSAGE
95 | const welcomeMsgUuid = 'WLC-MSG-NAME-' + Math.random().toString().slice(2, 10);
96 | const newWelcomeMessage = await v1Client.newWelcomeDm(welcomeMsgUuid, {
97 | text: 'New message text!',
98 | });
99 |
100 | const msgId = newWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].id;
101 | expect(newWelcomeMessage).to.haveOwnProperty(EDirectMessageEventTypeV1.WelcomeCreate);
102 | expect(newWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].message_data.text).to.equal('New message text!');
103 | expect(newWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].name).to.equal(welcomeMsgUuid);
104 |
105 | // GET WELCOME MESSAGE
106 | const existingWelcomeMessage = await v1Client.getWelcomeDm(msgId);
107 | expect(existingWelcomeMessage).to.haveOwnProperty(EDirectMessageEventTypeV1.WelcomeCreate);
108 | expect(existingWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].message_data.text).to.equal('New message text!');
109 | expect(existingWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].name).to.equal(welcomeMsgUuid);
110 |
111 | // UPDATE WELCOME MESSAGE
112 | const updatedMessage = await v1Client.updateWelcomeDm(msgId, { text: 'Updated message text!' });
113 | expect(updatedMessage[EDirectMessageEventTypeV1.WelcomeCreate].message_data.text).to.equal('Updated message text!');
114 |
115 | // LIST WELCOME MESSAGE (and ensure our sent DM is inside the list.)
116 | const welcomeMsgPaginator = await v1Client.listWelcomeDms();
117 | const allWelcomeDirectMessages = [] as ReceivedWelcomeDMCreateEventV1[];
118 |
119 | for await (const dm of welcomeMsgPaginator) {
120 | allWelcomeDirectMessages.push(dm);
121 | }
122 |
123 | expect(allWelcomeDirectMessages.some(msg => msg.id === msgId)).to.be.true;
124 |
125 | // Delete the welcome DM
126 | await v1Client.deleteWelcomeDm(msgId);
127 | }).timeout(120 * 1000);
128 |
129 | it('.newWelcomeDmRule/.getWelcomeDmRule/.listWelcomeDmRules/.deleteWelcomeDmRule/.setWelcomeDm - Every method related to welcome messages rules.', async () => {
130 | if (!isDmTestEnabled) {
131 | return;
132 | }
133 |
134 | const v1Client = client.v1;
135 | const currentRules = await v1Client.listWelcomeDmRules();
136 | // Cleanup every available dm rule
137 | for await (const rule of currentRules.welcome_message_rules ?? []) {
138 | await v1Client.deleteWelcomeDmRule(rule.id);
139 | }
140 |
141 | // NEW WELCOME MESSAGE
142 | const welcomeMsgUuid = 'WLC-MSG-NAME-' + Math.random().toString().slice(2, 10);
143 | const newWelcomeMessage = await v1Client.newWelcomeDm(welcomeMsgUuid, {
144 | text: 'New message text, for rule!',
145 | });
146 |
147 | const msgId = newWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].id;
148 |
149 | // Create the rule
150 | const rule = await v1Client.newWelcomeDmRule(msgId);
151 | expect(rule.welcome_message_rule.welcome_message_id).to.equal(msgId);
152 |
153 | await sleepSecs(4);
154 |
155 | // Create another welcome message
156 | const welcomeMsgUuid2 = 'WLC-MSG-NAME-' + Math.random().toString().slice(2, 10);
157 | const anotherWelcomeMessage = await v1Client.newWelcomeDm(welcomeMsgUuid2, {
158 | text: 'New message text, for rule (2)!',
159 | });
160 |
161 | const newMsgId = anotherWelcomeMessage[EDirectMessageEventTypeV1.WelcomeCreate].id;
162 |
163 | // Set another rule (will list and delete older rules)
164 | const newRule = await v1Client.setWelcomeDm(newMsgId);
165 |
166 | // Other rule should be deleted.
167 | // Delete new rule
168 | await v1Client.deleteWelcomeDmRule(newRule.welcome_message_rule.id);
169 | await v1Client.deleteWelcomeDm(newRule.welcome_message_rule.welcome_message_id);
170 | }).timeout(120 * 1000);
171 | });
172 |
--------------------------------------------------------------------------------
/test/list.v1.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getUserClient, sleepTest } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 |
8 | describe.skip('List endpoints for v1.1 API', () => {
9 | before(() => {
10 | client = getUserClient();
11 | });
12 |
13 | it('.createList/.updateList/.listOwnerships/.removeList/.list - Create, update, get and delete a list', async () => {
14 | const newList = await client.v1.createList({ name: 'cats', mode: 'private' });
15 |
16 | await sleepTest(1000);
17 | let createdList = await client.v1.list({ list_id: newList.id_str });
18 |
19 | expect(createdList.id_str).to.equal(newList.id_str);
20 |
21 | await client.v1.updateList({ list_id: newList.id_str, name: 'cats updated' });
22 | await sleepTest(1000);
23 | createdList = await client.v1.list({ list_id: newList.id_str });
24 | expect(createdList.name).to.equal('cats updated');
25 |
26 | const ownerships = await client.v1.listOwnerships();
27 | expect(ownerships.lists.some(l => l.id_str === newList.id_str)).to.equal(true);
28 |
29 | await sleepTest(1000);
30 | // This {does} works, but sometimes a 404 is returned...
31 | // eslint-disable-next-line @typescript-eslint/no-empty-function
32 | await client.v1.removeList({ list_id: newList.id_str }).catch(() => {});
33 | }).timeout(60 * 1000);
34 |
35 | it('.addListMembers/.removeListMembers/.listMembers/.listStatuses - Manage list members and list statuses', async () => {
36 | const newList = await client.v1.createList({ name: 'test list', mode: 'private' });
37 |
38 | await sleepTest(1000);
39 | await client.v1.addListMembers({ list_id: newList.id_str, user_id: '12' });
40 | await sleepTest(1000);
41 |
42 | const statuses = await client.v1.listStatuses({ list_id: newList.id_str });
43 | expect(statuses.tweets).to.have.length.greaterThan(0);
44 |
45 | const members = await client.v1.listMembers({ list_id: newList.id_str });
46 | expect(members.users.some(u => u.id_str === '12')).to.equal(true);
47 |
48 | await client.v1.removeListMembers({ list_id: newList.id_str, user_id: '12' });
49 |
50 | await sleepTest(1000);
51 | // eslint-disable-next-line @typescript-eslint/no-empty-function
52 | await client.v1.removeList({ list_id: newList.id_str }).catch(() => {});
53 | }).timeout(60 * 1000);
54 | });
55 |
--------------------------------------------------------------------------------
/test/media-upload.v1..test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { EUploadMimeType, TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 |
8 | let client: TwitterApi;
9 | const dirname = __dirname;
10 |
11 | const jpgImg = path.resolve(dirname, 'assets', 'lolo.jpg');
12 | const gifImg = path.resolve(dirname, 'assets', 'pec.gif');
13 | const mp4vid = path.resolve(dirname, 'assets', 'bbb.mp4');
14 | const maxTimeout = 1000 * 60;
15 |
16 | describe('Media upload for v1.1 API', () => {
17 | before(() => {
18 | client = getUserClient();
19 | });
20 |
21 | it('Upload a JPG image from filepath', async () => {
22 | // Upload media (from path)
23 | const fromPath = await client.v1.uploadMedia(jpgImg);
24 | expect(fromPath).to.be.an('string');
25 | expect(fromPath).to.have.length.greaterThan(0);
26 | }).timeout(maxTimeout);
27 |
28 | it('Upload a JPG image from file handle', async () => {
29 | // Upload media (from fileHandle)
30 | const fromHandle = await client.v1.uploadMedia(await fs.promises.open(jpgImg, 'r'), { mimeType: EUploadMimeType.Jpeg });
31 | expect(fromHandle).to.be.an('string');
32 | expect(fromHandle).to.have.length.greaterThan(0);
33 | }).timeout(maxTimeout);
34 |
35 | it('Upload a JPG image from numbered file handle', async () => {
36 | // Upload media (from numbered fileHandle)
37 | const fromNumberFh = await client.v1.uploadMedia(fs.openSync(jpgImg, 'r'), { mimeType: EUploadMimeType.Jpeg, maxConcurrentUploads: 1 });
38 | expect(fromNumberFh).to.be.an('string');
39 | expect(fromNumberFh).to.have.length.greaterThan(0);
40 | }).timeout(maxTimeout);
41 |
42 | it('Upload a GIF image from filepath', async () => {
43 | // Upload media (from path)
44 | const fromPath = await client.v1.uploadMedia(gifImg);
45 | expect(fromPath).to.be.an('string');
46 | expect(fromPath).to.have.length.greaterThan(0);
47 | }).timeout(maxTimeout);
48 |
49 | it('Upload a GIF image from buffer', async () => {
50 | // Upload media (from buffer)
51 | const fromBuffer = await client.v1.uploadMedia(await fs.promises.readFile(gifImg), { mimeType: EUploadMimeType.Gif });
52 | expect(fromBuffer).to.be.an('string');
53 | expect(fromBuffer).to.have.length.greaterThan(0);
54 | }).timeout(maxTimeout);
55 |
56 | it('Upload a MP4 video from path', async () => {
57 | const video = await client.v1.uploadMedia(mp4vid);
58 | expect(video).to.be.an('string');
59 | expect(video).to.have.length.greaterThan(0);
60 |
61 | const mediaInfo = await client.v1.mediaInfo(video);
62 | expect(mediaInfo.processing_info?.state).to.equal('succeeded');
63 | }).timeout(maxTimeout);
64 | });
65 |
--------------------------------------------------------------------------------
/test/media-upload.v2.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { EUploadMimeType, TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 |
8 | let client: TwitterApi;
9 |
10 | const gifImg = path.resolve(__dirname, 'assets', 'pec.gif');
11 | const maxTimeout = 1000 * 60;
12 |
13 | describe.skip('Media upload for v2 API', () => {
14 | before(() => {
15 | client = getUserClient();
16 | });
17 |
18 | it('Upload a GIF image from buffer', async () => {
19 | // Upload media (from buffer)
20 | const mediaId = await client.v2.uploadMedia(await fs.promises.readFile(gifImg), { media_type: EUploadMimeType.Gif });
21 | expect(mediaId).to.be.an('string');
22 | expect(mediaId).to.have.length.greaterThan(0);
23 | }).timeout(maxTimeout);
24 | });
--------------------------------------------------------------------------------
/test/plugin.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { getUserKeys, getRequestKeys } from '../src/test/utils';
4 | import type {
5 | ITwitterApiClientPlugin,
6 | TwitterResponse,
7 | ITwitterApiBeforeRequestConfigHookArgs,
8 | ITwitterApiAfterRequestHookArgs,
9 | ITwitterApiAfterOAuth1RequestTokenHookArgs,
10 | } from '../src';
11 | import { TwitterApi } from '../src';
12 |
13 | class SimpleCacheTestPlugin implements ITwitterApiClientPlugin {
14 | protected cache: { [urlHash: string]: TwitterResponse } = {};
15 |
16 | onBeforeRequestConfig(args: ITwitterApiBeforeRequestConfigHookArgs) {
17 | const hash = this.getHashFromRequest(args);
18 | return this.cache[hash];
19 | }
20 |
21 | onAfterRequest(args: ITwitterApiAfterRequestHookArgs) {
22 | const hash = this.getHashFromRequest(args);
23 | this.cache[hash] = args.response;
24 | }
25 |
26 | protected getHashFromRequest({ url, params }: ITwitterApiBeforeRequestConfigHookArgs) {
27 | const strQuery = JSON.stringify(params.query ?? {});
28 | const strParams = JSON.stringify(params.params ?? {});
29 |
30 | return params.method.toUpperCase() + ' ' + url.toString() + '|' + strQuery + '|' + strParams;
31 | }
32 | }
33 |
34 | class SimpleOAuthStepHelperPlugin implements ITwitterApiClientPlugin {
35 | protected cache: { [oauthToken: string]: string } = {};
36 |
37 | onOAuth1RequestToken(args: ITwitterApiAfterOAuth1RequestTokenHookArgs) {
38 | this.cache[args.oauthResult.oauth_token] = args.oauthResult.oauth_token_secret;
39 | }
40 |
41 | login(oauthToken: string, oauthVerifier: string) {
42 | if (!oauthVerifier || !this.isOAuthTokenValid(oauthToken)) {
43 | throw new Error('Invalid or expired token.');
44 | }
45 |
46 | const client = new TwitterApi({
47 | ...getRequestKeys(),
48 | accessToken: oauthToken,
49 | accessSecret: this.cache[oauthToken],
50 | });
51 |
52 | return client.login(oauthVerifier);
53 | }
54 |
55 | isOAuthTokenValid(oauthToken: string) {
56 | return !!this.cache[oauthToken];
57 | }
58 | }
59 |
60 | describe('Plugin API', () => {
61 | it('Cache a single request with a plugin', async () => {
62 | const client = new TwitterApi(getUserKeys(), { plugins: [new SimpleCacheTestPlugin()] });
63 |
64 | const user = await client.v1.verifyCredentials();
65 | const anotherRequest = await client.v1.verifyCredentials();
66 |
67 | expect(user).to.equal(anotherRequest);
68 | }).timeout(1000 * 30);
69 |
70 | it('Remember OAuth token secret between step 1 and 2 of authentication', async () => {
71 | const client = new TwitterApi(getRequestKeys(), { plugins: [new SimpleOAuthStepHelperPlugin()] });
72 |
73 | const { oauth_token } = await client.generateAuthLink('oob');
74 |
75 | // Is oauth token registered in cache?
76 | const loginPlugin = client.getPluginOfType(SimpleOAuthStepHelperPlugin)!;
77 | expect(loginPlugin.isOAuthTokenValid(oauth_token)).to.equal(true);
78 |
79 | // Must login through
80 | // const { client: loggedClient, accessToken, accessSecret } = await loginPlugin.login(oauth_token, 'xxxxxxxx');
81 | // - Save accessToken, accessSecret to persistent storage
82 | }).timeout(1000 * 30);
83 | });
84 |
--------------------------------------------------------------------------------
/test/space.v2.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getAppClient } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 |
8 | describe.skip('Spaces endpoints for v2 API', () => {
9 | before(async () => {
10 | client = await getAppClient();
11 | });
12 |
13 | it('.space/.spaces/.searchSpaces/.spacesByCreators - Lookup for spaces', async () => {
14 | const spacesBySearch = await client.v2.searchSpaces({
15 | query: 'twitter',
16 | state: 'live',
17 | 'space.fields': ['created_at', 'host_ids', 'title', 'lang', 'invited_user_ids', 'creator_id'],
18 | });
19 |
20 | expect(spacesBySearch.meta).to.be.a('object');
21 | expect(spacesBySearch.meta.result_count).to.be.a('number');
22 |
23 | if (spacesBySearch.data?.length) {
24 | const space = spacesBySearch.data[0];
25 | expect(space.created_at).to.be.a('string');
26 | expect(space.creator_id).to.be.a('string');
27 | expect(space.host_ids).to.be.a('array');
28 |
29 | const singleSpace = await client.v2.space(space.id);
30 | const singleSpaceThroughLookup = await client.v2.spaces([space.id]);
31 | const spacesOfCreator = await client.v2.spacesByCreators([space.creator_id!]);
32 |
33 | expect(singleSpace.data.id).to.equal(space.id);
34 |
35 | if (singleSpaceThroughLookup.data) {
36 | expect(singleSpaceThroughLookup.data[0].id).to.equal(space.id);
37 | }
38 |
39 | if (spacesOfCreator.data) {
40 | expect(spacesOfCreator.data.some(s => s.id === space.id)).to.equal(true);
41 | }
42 | }
43 | }).timeout(60 * 1000);
44 | });
45 |
--------------------------------------------------------------------------------
/test/stream.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi, ETwitterStreamEvent } from '../src';
4 | import { getAppClient, getUserClient } from '../src/test/utils';
5 |
6 | // OAuth 1.0a
7 | const clientOauth = getUserClient();
8 |
9 | describe.skip('Tweet stream API v1.1', () => {
10 | it('Should stream 3 tweets without any network error for statuses/filter using events', async () => {
11 | const streamv1Filter = await clientOauth.v1.filterStream({ track: 'JavaScript' });
12 |
13 | const numberOfTweets = await new Promise((resolve, reject) => {
14 | let numberOfTweets = 0;
15 |
16 | // Awaits for a tweet
17 | streamv1Filter.on(ETwitterStreamEvent.ConnectionError, reject);
18 | streamv1Filter.on(ETwitterStreamEvent.ConnectionClosed, reject);
19 | streamv1Filter.on(ETwitterStreamEvent.Data, () => {
20 | numberOfTweets++;
21 |
22 | if (numberOfTweets >= 3) {
23 | resolve(numberOfTweets);
24 | }
25 | });
26 | streamv1Filter.on(ETwitterStreamEvent.DataKeepAlive, () => console.log('Received keep alive event'));
27 | }).finally(() => {
28 | streamv1Filter.close();
29 | });
30 |
31 | expect(numberOfTweets).to.equal(3);
32 | }).timeout(1000 * 120);
33 | });
34 |
35 | describe.skip('Tweet stream API v2', () => {
36 | let clientBearer: TwitterApi;
37 |
38 | before(async () => {
39 | clientBearer = await getAppClient();
40 | });
41 |
42 | beforeEach(async () => {
43 | await new Promise(resolve => setTimeout(resolve, 1000));
44 | });
45 |
46 | it('Should stream 3 tweets without any network error for sample/stream using async iterator', async () => {
47 | const streamv2Sample = await clientBearer.v2.getStream('tweets/sample/stream');
48 |
49 | let numberOfTweets = 0;
50 |
51 | for await (const _ of streamv2Sample) {
52 | numberOfTweets++;
53 |
54 | if (numberOfTweets >= 3) {
55 | break;
56 | }
57 | }
58 |
59 | streamv2Sample.close();
60 |
61 | expect(numberOfTweets).to.equal(3);
62 | }).timeout(1000 * 120);
63 |
64 | it('In 10 seconds, should have the same tweets registered by async iterator and event handler, where stream is manually started', async () => {
65 | const streamV2 = clientBearer.v2.sampleStream({ autoConnect: false });
66 |
67 | const eventTweetIds = [] as string[];
68 | const itTweetIds = [] as string[];
69 |
70 | await streamV2.connect({ autoReconnect: true });
71 |
72 | await Promise.race([
73 | // 10 seconds timeout
74 | new Promise(resolve => setTimeout(resolve, 10 * 1000)),
75 | (async function () {
76 | streamV2.on(ETwitterStreamEvent.Data, tweet => eventTweetIds.push(tweet.data.id));
77 |
78 | for await (const tweet of streamV2) {
79 | itTweetIds.push(tweet.data.id);
80 | }
81 | })(),
82 | ]);
83 |
84 | streamV2.close();
85 |
86 | expect(eventTweetIds).to.have.length(itTweetIds.length);
87 | expect(eventTweetIds.every(id => itTweetIds.includes(id))).to.be.true;
88 | }).timeout(1000 * 120);
89 | });
90 |
--------------------------------------------------------------------------------
/test/tweet.v1.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 |
8 | describe.skip('Tweets endpoints for v1.1 API', () => {
9 | before(() => {
10 | client = getUserClient();
11 | });
12 |
13 | it.skip('.get - Get 2 tweets using raw HTTP method & specific endpoint', async () => {
14 | // Using raw HTTP method and URL
15 | const response1 = await client.get('https://api.x.com/1.1/search/tweets.json?q=@jack&count=2');
16 | // Using query parser
17 | const response2 = await client.v1.get('search/tweets.json', {
18 | q: 'jack',
19 | count: 2,
20 | });
21 |
22 | for (const response of [response1, response2]) {
23 | expect(response.statuses).to.have.length.lessThanOrEqual(2);
24 | const firstTweet = response.statuses[0];
25 |
26 | expect(firstTweet).to.haveOwnProperty('user');
27 | expect(firstTweet).to.haveOwnProperty('id_str');
28 |
29 | const firstUser = firstTweet.user;
30 | expect(firstUser).to.haveOwnProperty('id_str');
31 | }
32 | }).timeout(60 * 1000);
33 |
34 | it('.get - Get 2 tweets of a specific user', async () => {
35 | // Using raw HTTP method and URL
36 | const response1 = await client.get('https://api.x.com/1.1/statuses/user_timeline.json?screen_name=jack&count=2');
37 | // Using query parser
38 | const response2 = await client.v1.get('statuses/user_timeline.json', {
39 | screen_name: 'jack',
40 | count: 2,
41 | });
42 |
43 | for (const response of [response1, response2]) {
44 | expect(response).to.have.length.lessThanOrEqual(2);
45 | const firstTweet = response[0];
46 |
47 | expect(firstTweet).to.haveOwnProperty('user');
48 | expect(firstTweet).to.haveOwnProperty('id_str');
49 |
50 | const firstUser = firstTweet.user;
51 | expect(firstUser).to.haveOwnProperty('id_str');
52 | expect(firstUser).to.haveOwnProperty('screen_name');
53 | expect(firstUser.screen_name).to.equal('jack');
54 | }
55 |
56 | }).timeout(60 * 1000);
57 |
58 | it('.oembedTweet - Get a embed tweet', async () => {
59 | const embedTweet = await client.v1.oembedTweet('20');
60 | expect(embedTweet.html).to.be.a('string');
61 | }).timeout(60 * 1000);
62 |
63 | it('.singleTweet/.tweets - Get multiple tweets', async () => {
64 | const tweet = await client.v1.singleTweet('20');
65 | expect(tweet.id_str).to.be.eq('20');
66 |
67 | const tweets = await client.v1.tweets(['20', '12']);
68 | expect(tweets).to.be.an('array');
69 | expect(tweets).to.have.lengthOf(1);
70 |
71 | const tweetMap = await client.v1.tweets(['20', '12'], { map: true });
72 | expect(tweetMap).to.be.an('object');
73 | expect(tweetMap.id[12]).to.equal(null);
74 | expect(tweetMap.id[20]).to.be.an('object');
75 | }).timeout(60 * 1000);
76 | });
77 |
--------------------------------------------------------------------------------
/test/tweet.v2.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getAppClient, getUserClient, sleepTest } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 | const userClient = getUserClient();
8 |
9 | describe('Tweets endpoints for v2 API', () => {
10 | before(async () => {
11 | client = await getAppClient();
12 | });
13 |
14 | it.skip('.get - Get 2 tweets using raw HTTP method & specific endpoint', async () => {
15 | // Using raw HTTP method and URL
16 | const response1 = await client.get('https://api.x.com/2/tweets?ids=20,1306166445135605761&expansions=author_id&tweet.fields=public_metrics&user.fields=name,public_metrics');
17 | // Using query parser
18 | const response2 = await client.v2.get('tweets', {
19 | ids: '20,1306166445135605761',
20 | expansions: 'author_id',
21 | 'tweet.fields': 'public_metrics',
22 | 'user.fields': 'name,public_metrics',
23 | });
24 |
25 | for (const response of [response1, response2]) {
26 | expect(response.data).to.have.length.lessThanOrEqual(2);
27 | const firstTweet = response.data[0];
28 |
29 | expect(firstTweet).to.haveOwnProperty('author_id');
30 | expect(firstTweet).to.haveOwnProperty('public_metrics');
31 |
32 | const includes = response.includes?.users;
33 | const firstInclude = includes[0];
34 | expect(includes).to.have.length.lessThanOrEqual(2);
35 | expect(firstInclude).to.haveOwnProperty('name');
36 | }
37 |
38 | }).timeout(60 * 1000);
39 |
40 | it.skip('.search - Search and fetch tweets using tweet searcher and consume 200 tweets', async () => {
41 | // Using string for query
42 | const response1 = await client.v2.search('nodeJS');
43 | // Using object with query key
44 | const response2 = await client.v2.search({ query: 'nodeJS' });
45 |
46 | for (const response of [response1, response2]) {
47 | const originalLength = response.tweets.length;
48 | expect(response.tweets.length).to.be.greaterThan(0);
49 |
50 | await response.fetchNext();
51 | expect(response.tweets.length).to.be.greaterThan(originalLength);
52 |
53 | // Test if iterator correctly fetch tweets (silent)
54 | let i = 0;
55 | const ids = [];
56 |
57 | for await (const tweet of response) {
58 | ids.push(tweet.id);
59 | if (i > 200) {
60 | break;
61 | }
62 |
63 | i++;
64 | }
65 | // Check for duplicates
66 | expect(ids).to.have.length(new Set(ids).size);
67 | }
68 | }).timeout(60 * 1000);
69 |
70 | it.skip('.userTimeline/.userMentionTimeline - Fetch user & mention timeline and consume 150 tweets', async () => {
71 | const jackTimeline = await client.v2.userTimeline('12');
72 |
73 | const originalLength = jackTimeline.tweets.length;
74 | expect(jackTimeline.tweets.length).to.be.greaterThan(0);
75 |
76 | await jackTimeline.fetchNext();
77 | expect(jackTimeline.tweets.length).to.be.greaterThan(originalLength);
78 |
79 | const nextPage = await jackTimeline.next();
80 | expect(nextPage.tweets.map(t => t.id))
81 | .to.not.have.members(jackTimeline.tweets.map(t => t.id));
82 |
83 | // Test if iterator correctly fetch tweets (silent)
84 | let i = 0;
85 | const ids = [];
86 |
87 | for await (const tweet of jackTimeline) {
88 | ids.push(tweet.id);
89 | if (i > 150) {
90 | break;
91 | }
92 |
93 | i++;
94 | }
95 |
96 | // Check for duplicates
97 | expect(ids).to.have.length(new Set(ids).size);
98 |
99 | // Test mentions
100 | const jackMentions = await client.v2.userMentionTimeline('12', {
101 | 'tweet.fields': ['author_id', 'in_reply_to_user_id'],
102 | });
103 | expect(jackMentions.tweets.length).to.be.greaterThan(0);
104 | expect(jackMentions.tweets.map(tweet => tweet.author_id)).to.not.include('12');
105 | }).timeout(60 * 1000);
106 |
107 | it.skip('.singleTweet - Download a single tweet', async () => {
108 | const tweet = await client.v2.singleTweet('20');
109 | expect(tweet.data.text).to.equal('just setting up my twttr');
110 | }).timeout(60 * 1000);
111 |
112 | it.skip('.tweetWithMedia - Get a tweet with media variants', async () => {
113 | const tweet = await client.v2.singleTweet('870042717589340160', { 'tweet.fields': ['attachments'], 'expansions': ['attachments.media_keys'], 'media.fields': ['variants'] });
114 | expect(tweet.includes && tweet.includes.media && tweet.includes.media[0].variants && tweet.includes.media[0].variants[0].content_type).to.equal('video/mp4');
115 | }).timeout(60 * 1000);
116 |
117 | it.skip('.tweets - Fetch a bunch of tweets', async () => {
118 | const tweets = await client.v2.tweets(['20', '1257577057862610951'], {
119 | 'tweet.fields': ['author_id'],
120 | });
121 | expect(tweets.data).to.have.length(2);
122 |
123 | const first = tweets.data[0];
124 | expect(first.author_id).to.be.a('string');
125 | }).timeout(60 * 1000);
126 |
127 | it.skip('.like/.unlike - Like / unlike a single tweet', async () => {
128 | const me = await userClient.currentUser();
129 | const { data: { liked } } = await userClient.v2.like(me.id_str, '20');
130 | expect(liked).to.equal(true);
131 |
132 | await sleepTest(300);
133 |
134 | const { data: { liked: likedAfterUnlike } } = await userClient.v2.unlike(me.id_str, '20');
135 | expect(likedAfterUnlike).to.equal(false);
136 | }).timeout(60 * 1000);
137 |
138 | it.skip('.tweetLikedBy - Get users that liked a tweet', async () => {
139 | const usersThatLiked = await userClient.v2.tweetLikedBy('20', { 'user.fields': ['created_at'] });
140 | expect(usersThatLiked.data).to.have.length.greaterThan(0);
141 |
142 | expect(usersThatLiked.data[0].created_at).to.be.a('string');
143 | }).timeout(60 * 1000);
144 |
145 | it.skip('.tweetRetweetedBy - Get users that retweeted a tweet', async () => {
146 | const usersThatRt = await userClient.v2.tweetRetweetedBy('20', { 'user.fields': ['created_at'] });
147 | expect(usersThatRt.data).to.have.length.greaterThan(0);
148 |
149 | expect(usersThatRt.data[0].created_at).to.be.a('string');
150 | }).timeout(60 * 1000);
151 |
152 | it('.tweet/.deleteTweet - Creates a tweet then delete it', async () => {
153 | const status = '[TEST THIS IS A TEST TWEET.]';
154 |
155 | const { data: { text, id } } = await userClient.v2.tweet(status);
156 | expect(text).to.equal(status);
157 |
158 | const { data: { deleted } } = await userClient.v2.deleteTweet(id);
159 | expect(deleted).to.equal(true);
160 | }).timeout(60 * 1000);
161 | });
162 |
--------------------------------------------------------------------------------
/test/user.v1.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi } from '../src';
4 | import { getUserClient } from '../src/test/utils';
5 |
6 | let userClient: TwitterApi;
7 |
8 | describe.skip('Users endpoints for v1.1 API', () => {
9 | before(async () => {
10 | userClient = getUserClient();
11 | });
12 |
13 | it('.user/.users - Get users by ID', async () => {
14 | const jack = await userClient.v1.user({ user_id: '12' });
15 |
16 | expect(jack).to.be.a('object');
17 | expect(jack.id_str).to.equal('12');
18 | expect(jack.screen_name.toLowerCase()).to.equal('jack');
19 |
20 | const users = await userClient.v1.users({ user_id: ['12', '14561327'] });
21 | expect(users).to.have.length(2);
22 | }).timeout(60 * 1000);
23 |
24 | it('.searchUsers - Search for users', async () => {
25 | const jackSearch = await userClient.v1.searchUsers('jack');
26 |
27 | expect(jackSearch.users).to.be.a('array');
28 | expect(jackSearch.users).to.have.length.greaterThan(0);
29 | }).timeout(60 * 1000);
30 |
31 | it('.userProfileBannerSizes - Get banner size of a user', async () => {
32 | const sizes = await userClient.v1.userProfileBannerSizes({ user_id: '14561327' });
33 |
34 | expect(sizes.sizes).to.be.a('object');
35 | expect(sizes.sizes.web_retina.h).to.be.a('number');
36 | }).timeout(60 * 1000);
37 |
38 | it('.friendship/.friendships - Get friendship objects', async () => {
39 | const friendship = await userClient.v1.friendship({ source_id: '12', target_id: '14561327' });
40 | expect(friendship.relationship).to.be.an('object');
41 | expect(friendship.relationship.source.id_str).to.eq('12');
42 | expect(friendship.relationship.target.id_str).to.eq('14561327');
43 |
44 | const friendships = await userClient.v1.friendships({ user_id: ['12', '786491'] });
45 | expect(friendships).to.be.an('array');
46 | expect(friendships).to.have.lengthOf(2);
47 | expect(friendships[0].id_str).to.be.oneOf(['12', '786491']);
48 | }).timeout(60 * 1000);
49 | });
50 |
51 |
52 | // describe('', () => {
53 | // it('', async () => {
54 |
55 | // }).timeout(60 * 1000);
56 | // });
57 |
--------------------------------------------------------------------------------
/test/user.v2.test.ts:
--------------------------------------------------------------------------------
1 | import 'mocha';
2 | import { expect } from 'chai';
3 | import { TwitterApi, TwitterApiReadOnly } from '../src';
4 | import { getAppClient, getUserClient } from '../src/test/utils';
5 |
6 | let client: TwitterApi;
7 | let roClient: TwitterApiReadOnly;
8 | let userClient: TwitterApi;
9 |
10 | describe.skip('Users endpoints for v2 API', () => {
11 | before(async () => {
12 | client = await getAppClient();
13 | roClient = client.readOnly;
14 | userClient = getUserClient();
15 | });
16 |
17 | it('.user/.users - Get users by ID', async () => {
18 | const jack = await roClient.v2.user('12', {
19 | 'expansions': ['pinned_tweet_id'],
20 | 'tweet.fields': ['lang'],
21 | 'user.fields': 'username',
22 | });
23 |
24 | expect(jack.data).to.be.a('object');
25 | expect(jack.data.id).to.equal('12');
26 | expect(jack.data.username.toLowerCase()).to.equal('jack');
27 |
28 | if (jack.data.pinned_tweet_id) {
29 | expect(jack.includes!).to.be.a('object');
30 | expect(jack.includes!.tweets).to.have.length.greaterThan(0);
31 | expect(jack.includes!.tweets![0]).to.haveOwnProperty('lang');
32 | }
33 |
34 | const users = await roClient.v2.users(['12', '14561327']);
35 | expect(users.data).to.have.length(2);
36 | }).timeout(60 * 1000);
37 |
38 | it('.userByUsername/.usersByUsernames - Get users by screen names', async () => {
39 | const jack = await roClient.v2.userByUsername('jack', {
40 | 'expansions': ['pinned_tweet_id'],
41 | 'tweet.fields': ['lang'],
42 | 'user.fields': 'username',
43 | });
44 |
45 | expect(jack.data).to.be.a('object');
46 | expect(jack.data.id).to.equal('12');
47 | expect(jack.data.username.toLowerCase()).to.equal('jack');
48 |
49 | if (jack.data.pinned_tweet_id) {
50 | expect(jack.includes!).to.be.a('object');
51 | expect(jack.includes!.tweets).to.have.length.greaterThan(0);
52 | expect(jack.includes!.tweets![0]).to.haveOwnProperty('lang');
53 | }
54 |
55 | const users = await roClient.v2.usersByUsernames(['jack', 'dhh']);
56 | expect(users.data).to.have.length(2);
57 | }).timeout(60 * 1000);
58 |
59 | it('.followers/.following - Get relationships of user', async () => {
60 | const followersOfJack = await roClient.v2.followers('12', {
61 | 'expansions': ['pinned_tweet_id'],
62 | 'tweet.fields': ['lang'],
63 | 'user.fields': 'username',
64 | });
65 |
66 | expect(followersOfJack.data).to.be.a('array');
67 | expect(followersOfJack.data).to.have.length.greaterThan(0);
68 |
69 | if (followersOfJack.includes?.tweets?.length) {
70 | expect(followersOfJack.includes.tweets).to.have.length.greaterThan(0);
71 | expect(followersOfJack.includes.tweets[0]).to.haveOwnProperty('lang');
72 | }
73 |
74 | const followingsOfJack = await roClient.v2.following('12', {
75 | 'expansions': ['pinned_tweet_id'],
76 | 'tweet.fields': ['lang'],
77 | 'user.fields': 'username',
78 | });
79 |
80 | expect(followingsOfJack.data).to.be.a('array');
81 | expect(followingsOfJack.data).to.have.length.greaterThan(0);
82 |
83 | if (followingsOfJack.includes?.tweets?.length) {
84 | expect(followingsOfJack.includes.tweets).to.have.length.greaterThan(0);
85 | expect(followingsOfJack.includes.tweets[0]).to.haveOwnProperty('lang');
86 | }
87 | }).timeout(60 * 1000);
88 |
89 | it('.follow/.unfollow - Follow/unfollow a user', async () => {
90 | const { readOnly, readWrite } = userClient;
91 |
92 | const currentUser = await readOnly.currentUser();
93 | // Follow then unfollow jack
94 | const followInfo = await readWrite.v2.follow(currentUser.id_str, '12');
95 | expect(followInfo.data.following).to.equal(true);
96 |
97 | // Sleep 2 seconds
98 | await new Promise(resolve => setTimeout(resolve, 1000 * 2));
99 |
100 | // Unfollow jack
101 | const unfollowInfo = await readWrite.v2.unfollow(currentUser.id_str, '12');
102 | expect(unfollowInfo.data.following).to.equal(false);
103 | }).timeout(60 * 1000);
104 |
105 | it('.block/.unblock/.userBlockingUsers - Block, list then unblock a user', async () => {
106 | const { readOnly, readWrite } = userClient;
107 |
108 | const currentUser = await readOnly.currentUser();
109 | // Block jack
110 | const blockInfo = await readWrite.v2.block(currentUser.id_str, '12');
111 | expect(blockInfo.data.blocking).to.equal(true);
112 |
113 | // Sleep 2 seconds
114 | await new Promise(resolve => setTimeout(resolve, 1000 * 2));
115 |
116 | const blocksOfUser = await readWrite.v2.userBlockingUsers(currentUser.id_str, { 'user.fields': ['created_at', 'protected'] });
117 |
118 | expect(blocksOfUser.users).to.have.length.greaterThan(0);
119 |
120 | const firstBlockedUser = blocksOfUser.users[0];
121 | const lengthInitial = blocksOfUser.users.length;
122 |
123 | expect(firstBlockedUser.id).to.be.a('string');
124 | expect(firstBlockedUser.created_at).to.be.a('string');
125 | expect(firstBlockedUser.protected).to.be.a('boolean');
126 |
127 | if (blocksOfUser.meta.next_token) {
128 | await blocksOfUser.fetchNext();
129 | expect(lengthInitial).to.not.equal(blocksOfUser.users.length);
130 | }
131 |
132 | // Sleep 2 seconds
133 | await new Promise(resolve => setTimeout(resolve, 1000 * 2));
134 |
135 | // unblock jack
136 | const unblockInfo = await readWrite.v2.unblock(currentUser.id_str, '12');
137 | expect(unblockInfo.data.blocking).to.equal(false);
138 | }).timeout(60 * 1000);
139 |
140 | it('.userLikedTweets - Last tweets liked by a user', async () => {
141 | const { readOnly } = userClient;
142 |
143 | const jackLikedTweets = await readOnly.v2.userLikedTweets('12', { 'tweet.fields': ['created_at'] });
144 | expect(jackLikedTweets.tweets).to.have.length.greaterThan(0);
145 |
146 | expect(jackLikedTweets.tweets[0].created_at).to.be.a('string');
147 | expect(jackLikedTweets.meta.next_token).to.be.a('string');
148 | }).timeout(60 * 1000);
149 | });
150 |
151 |
152 | // describe('', () => {
153 | // it('', async () => {
154 |
155 | // }).timeout(60 * 1000);
156 | // });
157 |
--------------------------------------------------------------------------------
/tsconfig.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
5 | "outDir": "dist/cjs", /* Redirect output structure to the directory. */
6 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
5 | "outDir": "dist/esm", /* Redirect output structure to the directory. */
6 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | // "outDir": "dist", /* Redirect output structure to the directory. */
18 | // "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | "allowSyntheticDefaultImports": false, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true, /* Skip type checking of declaration files. */
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | },
69 | "include": ["./src/**/*"],
70 | "exclude": ["./test/**/*"]
71 | }
72 |
--------------------------------------------------------------------------------