diff --git a/README.md b/README.md index c5536e3..d1aa08c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Here's the detailed feature list of `twitter-api-v2`: ### Basics: - Support for v1.1 and **v2 of Twitter API** -- Make signed HTTP requests to Twitter with every auth type: **OAuth 1.0a**, **OAuth2** and **Basic** HTTP Authorization +- Make signed HTTP requests to Twitter with every auth type: **OAuth 1.0a**, **OAuth2** (even brand new user context OAuth2!) and **Basic** HTTP Authorization - Helpers for numerous HTTP request methods (`GET`, `POST`, `PUT`, `DELETE` and `PATCH`), that handle query string parse & format, automatic body formatting and more - High-class support for stream endpoints, with easy data consumption and auto-reconnect on stream errors @@ -107,7 +107,7 @@ Learn how to use the full potential of `twitter-api-v2`. - Get started - [Create a client and make your first request](./doc/basics.md) - - [Handle Twitter authentification flows](./doc/auth.md) + - [Handle Twitter authentication flows](./doc/auth.md) - [Explore some examples](./doc/examples.md) - Use endpoints wrappers — ensure typings of request & response - [Available endpoint wrappers for v1.1 API](./doc/v1.md) diff --git a/changelog.md b/changelog.md index 3b96e60..3edc8f4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,7 @@ 1.7.2 ----- - Fix: Paginator can return multiple times the same results in some conditions -- Feat: .done properties for paginators, to know when a next page is fetchable +- Feat: .done property for paginators, to know when a next page is fetchable 1.7.1 ----- diff --git a/doc/auth.md b/doc/auth.md index 6754a60..125c723 100644 --- a/doc/auth.md +++ b/doc/auth.md @@ -1,24 +1,25 @@ -# Authentification +# Authentication -This part will guide you through the multiple steps of Twitter API authentification process +This part will guide you through the multiple steps of Twitter API authentication process inside `twitter-api-v2` package. Please first see the [Basics](./basics.md) to know how to create a client with your application keys. -***First***, you must know which type of authentification you want to use. +***First***, you must know which type of authentication you want to use. -- User authentification (3-legged OAuth 1.0a flow, see [User-wide authentification flow](#user-wide-authentification-flow)) -- App-only authentification (Bearer token, see [Application-only authentification flow](#application-only-authentification-flow)) -- Basic authentification (couple of username+password, see [Basic authentification flow](#basic-authentification-flow)) +- User authentication (3-legged OAuth 1.0a flow, see [User-wide authentication flow](#user-wide-authentication-flow)) +- App-only authentication (Bearer token, see [Application-only authentication flow](#application-only-authentication-flow)) +- Basic authentication (couple of username+password, see [Basic authentication flow](#basic-authentication-flow)) +- User authentication, but with fine-grained scopes (3-legged OAuth 2 flow, see [User-wide authentication flow for OAuth2](#oauth2-user-wide-authentication-flow)) **Note**: You can find a [project real-life example of a 3-legged auth flow here](https://github.com/alkihis/twitter-api-v2-user-oauth-flow-example). -## User-wide authentification flow +## User-wide authentication flow Many endpoints on the Twitter developer platform use the OAuth 1.0a method to act on behalf of a Twitter account. For example, if you have a Twitter developer app, you can make API requests on behalf of any Twitter account as long as that user authenticates your app. -This method is **fairly the most complex** of authentification flow options, but it is, at least for now, the **most used method across Twitter API**. +This method is **fairly the most complex** of authentication flow options, but it is, at least for now, the **most used method across Twitter API**. It is named "3-legged" because it is splitted in 3 parts: 1. You (the app/server) generate a auth link that is clickable by a external user, and gives you *temporary* access tokens @@ -40,8 +41,8 @@ You need to have a client instantiated with your **consumer keys** from Twitter. const client = new TwitterApi({ appKey: CONSUMER_KEY, appSecret: CONSUMER_SECRET }); ``` -To create the authentification link, use `client.generateAuthLink()` method. -**If you choose to redirect users to your website after authentification, you need to provide a callback URL here.** +To create the authentication link, use `client.generateAuthLink()` method. +**If you choose to redirect users to your website after authentication, you need to provide a callback URL here.** ```ts const authLink = await client.generateAuthLink(CALLBACK_URL); @@ -92,7 +93,7 @@ app.get('/callback', (req, res) => { client.login(oauth_verifier) .then(({ client: loggedClient, accessToken, accessSecret }) => { - // loggedClient is an authentificated client in behalf of some user + // loggedClient is an authenticated client in behalf of some user // Store accessToken & accessSecret somewhere }) .catch(() => res.status(403).send('Invalid verifier or access tokens!')); @@ -118,7 +119,7 @@ const client = new TwitterApi({ // Give the PIN to client.login() const { client: loggedClient, accessToken, accessSecret } = await client.login(GIVEN_USER_PIN); -// loggedClient is an authentificated client in behalf of some user +// loggedClient is an authenticated client in behalf of some user // Store accessToken & accessSecret somewhere ``` @@ -129,7 +130,7 @@ You can use the method `.currentUser()` on your client. This a shortcut to `.v1.verifyCredentials()` with a **cache that store user to avoid multiple API calls**. Its returns a `UserV1` object. -## Application-only authentification flow +## Application-only authentication flow App-only flow use a single OAuth 2.0 Bearer Token that authenticates requests on behalf of your developer App. As this method is specific to the App, it does not involve any users. @@ -147,7 +148,7 @@ const consumerClient = new TwitterApi({ appKey: CONSUMER_KEY, appSecret: CONSUME const client = await consumerClient.appLogin(); ``` -## Basic authentification flow +## Basic authentication flow Mainly for **Twitter enterprise APIs**, that require the use of HTTP Basic Authentication. You must pass a valid email address and password combination for each request. @@ -157,3 +158,99 @@ Use this combination to create your Twitter API client: ```ts const client = new TwitterApi({ username: MY_USERNAME, password: MY_PASSWORD }); ``` + +## OAuth2 user-wide authentication flow + +Alternatively of OAuth 1.0a method, you can use OAuth2 user-context, which is restricted to **v2 of Twitter API**. +This process is very similar of one used in OAuth 1.0a, so it's recommand to read it first to understand what's happening below. + +The main advantage of this method is that you can **explicitly specify which part of data you'll need from the Twitter user's account**. +These parts are called **scopes**. + +This authentification is splitted into 3 parts: +1. You (the app/server) generate a auth link with your client ID that is clickable by an external user +2. The user clicks on the link, approves the application, it gives you a client code +3. You use a code verifier generated at the first step along the client code to obtain **user-specific** access token; this token has a dedicated lifetime that can be extended with refresh tokens + +**NOTE** +> - If you're building a server that serves content for users, +> you need to "remember" (store) some data between the first two steps, +> so be sure you have a available session-like store (file/memory/Redis/...) to share data across same-user requests. +> - Between steps 1 & 2, users are redirected to official Twitter website. That means you need to have a dedicated page in your website meant to "welcome back" users that have been sent to Twitter (this is called **oauth callback**) + +### Create the auth link + +You need to have a client instantiated with your **client keys** from Twitter. +If you've declared app as "public" app, you only need your **client ID**, if you've declared app as "confidential" app, you will need **client ID and client secret**. + +```ts +const client = new TwitterApi({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }); +``` + +To create the authentication link, use `client.generateAuthLink()` method. +**If you choose to redirect users to your website after authentication, you need to provide a callback URL here.** +```ts +// Don't forget to specify 'offline.access' in scope list if you want to refresh your token later +const { url, codeVerifier, state } = client.generateOAuth2AuthLink(CALLBACK_URL, { scope: ['tweet.read', 'users.read', 'offline.access', ...] }); + +// Redirect your user to {url}, store {state} and {codeVerifier} into a DB/Redis/memory after user redirection +``` + +**IMPORTANT**: You need to store `state` and `codeVerifier` somewhere, +because you will need them for step 2. + +### Collect returned auth codes and get access token + +When Twitter redirects to your page, it provides two query string parameters: `code` and `state`. + +**NOTE**: If the user refuses app access, `code` will not be provided. + +You need to extract those tokens, find the linked `codeVerifier` from given `state` (using your session store!), then ask for accesss token. + +Create a client with your **client ID** (and the **client secret** if it's needed), like at step 1. + +An example flow will be written here using the **express** framework, feel free to adapt to your case. + +```ts +app.get('/callback', (req, res) => { + // Exact state and code from query string + const { state, code } = req.query; + // Get the saved oauth_token_secret from session + const { codeVerifier, state: sessionState } = req.session; + + if (!codeVerifier || !state || !sessionState || !code) { + return res.status(400).send('You denied the app or your session expired!'); + } + if (state !== sessionState) { + return res.status(400).send('Stored tokens didnt match!'); + } + + // Obtain access token + const client = new TwitterApi({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }); + + client.loginWithOAuth2({ code, codeVerifier, redirectUri: CALLBACK_URL }) + .then(({ client: loggedClient, accessToken, refreshToken, expiresIn }) => { + // {loggedClient} is an authenticated client in behalf of some user + // Store {accessToken} somewhere, it will be valid until {expiresIn} is hit. + // If you want to refresh your token later, store {refreshToken} (it is present if 'offline.access' has been given as scope) + + // Example request + const { data: userObject } = await loggedClient.v2.me(); + }) + .catch(() => res.status(403).send('Invalid verifier or access tokens!')); +}); +``` + +### Optional: refresh the token later + +If you choose to include `'offline.access'` as scope, you can store and re-use later `refreshToken` when `expiresIn` time kicks in. + +```ts +// Obtain {refreshToken} from your DB/store +const { client: refreshedClient, accessToken, refreshToken: newRefreshToken } = await client.refreshOAuth2Token(refreshToken); + +// Store refreshed {accessToken} and {newRefreshToken} to remplace the old ones + +// Example request +await refreshedClient.v2.me(); +``` diff --git a/doc/basics.md b/doc/basics.md index a92359f..16dd354 100644 --- a/doc/basics.md +++ b/doc/basics.md @@ -25,7 +25,7 @@ import { TwitterApi } from 'twitter-api-v2'; const { TwitterApi } = require('twitter-api-v2'); ``` -Instanciate with your wanted authentification method. +Instanciate with your wanted authentication method. ```ts // OAuth 1.0a (User context) @@ -57,9 +57,9 @@ you can choose the right sub-client: - `Read-write`: `rwClient = client.readWrite` - `Read-only`: `roClient = client.readOnly` -## Authentification +## Authentication -Please see [Authentification part](./auth.md) of the doc. +Please see [Authentication part](./auth.md) of the doc. ### Get current user diff --git a/doc/examples.md b/doc/examples.md index 99a3cdd..88744dd 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -199,7 +199,7 @@ await client.v1.setWelcomeDm(welcomeDm[EDirectMessageEventTypeV1.WelcomeCreate]. You can see a [real-life example of a 3-legged auth flow here](https://github.com/alkihis/twitter-api-v2-user-oauth-flow-example). -See also [authentification documentation](./auth.md) for examples and explainations about Twitter auth flow. +See also [authentication documentation](./auth.md) for examples and explainations about Twitter auth flow. ### Generate a auth link and get access tokens diff --git a/doc/streaming.md b/doc/streaming.md index 02f86f3..79901e6 100644 --- a/doc/streaming.md +++ b/doc/streaming.md @@ -101,7 +101,7 @@ await stream.connect({ autoReconnect: true, autoReconnectRetries: Infinity }); ## Specific API v1.1 implementations -API v1.1 streaming-related endpoints works only with classic OAuth 1.0a authentification. +API v1.1 streaming-related endpoints works only with classic OAuth 1.0a authentication. ### Filter endpoint @@ -145,7 +145,7 @@ const stream = await client.v1.sampleStream(); ## Specific API v2 implementations -API v2 streaming-related endpoints works only with Bearer OAuth2 authentification. +API v2 streaming-related endpoints works only with Bearer OAuth2 authentication. ### Search endpoint diff --git a/src/client-mixins/oauth2.helper.ts b/src/client-mixins/oauth2.helper.ts index 9169e3f..2f3351f 100644 --- a/src/client-mixins/oauth2.helper.ts +++ b/src/client-mixins/oauth2.helper.ts @@ -14,6 +14,11 @@ export class OAuth2Helper { ); } + static getAuthHeader(clientId: string, clientSecret: string) { + const key = encodeURIComponent(clientId) + ':' + encodeURIComponent(clientSecret); + return Buffer.from(key).toString('base64');; + } + static generateRandomString(length: number) { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; diff --git a/src/client-mixins/request-maker.mixin.ts b/src/client-mixins/request-maker.mixin.ts index 80d251b..10cf0ee 100644 --- a/src/client-mixins/request-maker.mixin.ts +++ b/src/client-mixins/request-maker.mixin.ts @@ -5,6 +5,7 @@ import { trimUndefinedProperties } from '../helpers'; import OAuth1Helper from './oauth1.helper'; import RequestHandlerHelper from './request-handler.helper'; import RequestParamHelpers from './request-param.helper'; +import { OAuth2Helper } from './oauth2.helper'; export type TRequestFullData = { url: URL, @@ -66,6 +67,7 @@ export abstract class ClientRequestMaker { protected _accessSecret?: string; protected _basicToken?: string; protected _clientId?: string; + protected _clientSecret?: string; protected _oauth?: OAuth1Helper; protected _rateLimits: { [endpoint: string]: TwitterRateLimit } = {}; @@ -165,6 +167,10 @@ export abstract class ClientRequestMaker { // Basic auth, to request a bearer token headers.Authorization = 'Basic ' + this._basicToken; } + else if (this._clientId && this._clientSecret) { + // Basic auth with clientId + clientSecret + headers.Authorization = 'Basic ' + OAuth2Helper.getAuthHeader(this._clientId, this._clientSecret); + } else if (this._consumerSecret && this._oauth) { // Merge query and body const data = bodyInSignature ? RequestParamHelpers.mergeQueryAndBodyForOAuth(query, body) : query; diff --git a/src/client.base.ts b/src/client.base.ts index 2adda0e..c238526 100644 --- a/src/client.base.ts +++ b/src/client.base.ts @@ -1,4 +1,4 @@ -import { TClientTokens, TwitterApiBasicAuth, TwitterApiOAuth2Init, TwitterApiTokens, TwitterRateLimit, TwitterResponse, UserV1 } from './types'; +import type { TClientTokens, TwitterApiBasicAuth, TwitterApiOAuth2Init, TwitterApiTokens, TwitterRateLimit, TwitterResponse, UserV1, UserV2Result } from './types'; import { ClientRequestMaker, TCustomizableRequestArgs, @@ -46,17 +46,18 @@ export type TStreamClientRequestArgsWithoutAutoConnect = TStreamClientRequestArg export default abstract class TwitterApiBase extends ClientRequestMaker { protected _prefix: string | undefined; protected _currentUser: UserV1 | null = null; + protected _currentUserV2: UserV2Result | null = null; /** - * Create a new TwitterApi object without authentification. + * Create a new TwitterApi object without authentication. */ constructor(); /** - * Create a new TwitterApi object with OAuth 2.0 Bearer authentification. + * Create a new TwitterApi object with OAuth 2.0 Bearer authentication. */ constructor(bearerToken: string); /** - * Create a new TwitterApi object with three-legged OAuth 1.0a authentification. + * Create a new TwitterApi object with three-legged OAuth 1.0a authentication. */ constructor(tokens: TwitterApiTokens); /** @@ -64,7 +65,7 @@ export default abstract class TwitterApiBase extends ClientRequestMaker { */ constructor(oauth2Init: TwitterApiOAuth2Init); /** - * Create a new TwitterApi object with Basic HTTP authentification. + * Create a new TwitterApi object with Basic HTTP authentication. */ constructor(credentials: TwitterApiBasicAuth); /** @@ -88,6 +89,7 @@ export default abstract class TwitterApiBase extends ClientRequestMaker { this._bearerToken = token._bearerToken; this._basicToken = token._basicToken; this._clientId = token._clientId; + this._clientSecret = token._clientSecret; this._rateLimits = token._rateLimits; } else if (typeof token === 'object' && 'appKey' in token) { @@ -107,6 +109,7 @@ export default abstract class TwitterApiBase extends ClientRequestMaker { } else if (typeof token === 'object' && 'clientId' in token) { this._clientId = token.clientId; + this._clientSecret = token.clientSecret; } } @@ -208,6 +211,25 @@ export default abstract class TwitterApiBase extends ClientRequestMaker { return currentUser; } + /** + * Get cached current user from v2 API. + * This can only be the slimest available `UserV2` object, with only `id`, `name` and `username` properties defined. + * + * To get a customized `UserV2Result`, use `.v2.me()` + * + * OAuth2 scopes: `tweet.read` & `users.read` + */ + protected async getCurrentUserV2Object(forceFetch = false) { + if (!forceFetch && this._currentUserV2) { + return this._currentUserV2; + } + + const currentUserV2 = await this.get('users/me', undefined, { prefix: 'https://api.twitter.com/2/' }); + this._currentUserV2 = currentUserV2; + + return currentUserV2; + } + /* Direct HTTP methods */ async get(url: string, query?: TRequestQuery, args?: TGetClientRequestArgsDataResponse) : Promise; diff --git a/src/client/readonly.ts b/src/client/readonly.ts index 0cf8d50..1e022fc 100644 --- a/src/client/readonly.ts +++ b/src/client/readonly.ts @@ -37,9 +37,9 @@ export default class TwitterApiReadOnly extends TwitterApiBase { /** * Fetch and cache current user. - * This method can only be called with a OAuth 1.0a user authentification. + * This method can only be called with a OAuth 1.0a user authentication. * - * You can use this method to test if authentification was successful. + * You can use this method to test if authentication was successful. * Next calls to this methods will use the cached user, unless `forceFetch: true` is given. */ public async currentUser(forceFetch = false) { @@ -52,7 +52,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { return this.v2.search(what, options); } - /* Authentification */ + /* Authentication */ /** * Generate the OAuth request token link for user-based OAuth 1.0 auth. @@ -139,7 +139,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { } /** - * Enable application-only authentification. + * Enable application-only authentication. * * To make the request, instanciate TwitterApi with consumer and secret. * @@ -154,7 +154,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { if (!this._consumerToken || !this._consumerSecret) throw new Error('You must setup TwitterApi instance with consumers to enable app-only login'); - // Create a client with Basic authentification + // Create a client with Basic authentication const basicClient = new TwitterApi({ username: this._consumerToken, password: this._consumerSecret }); const res = await basicClient.post('https://api.twitter.com/oauth2/token', { grant_type: 'client_credentials' }); @@ -162,11 +162,16 @@ export default class TwitterApiReadOnly extends TwitterApiBase { return new TwitterApi(res.access_token); } - /* OAuth 2 user authentification */ + /* OAuth 2 user authentication */ /** * Generate the OAuth request token link for user-based OAuth 2.0 auth. * + * - **You can only use v2 API endpoints with this authentication method.** + * - **You need to specify which scope you want to have when you create your auth link. Make sure it matches your needs.** + * + * See https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token for details. + * * ```ts * // Instanciate TwitterApi with client ID * const client = new TwitterApi({ clientId: 'yourClientId' }); @@ -184,7 +189,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { generateOAuth2AuthLink(redirectUri: string, options: Partial = {}) { if (!this._clientId) { throw new Error( - 'Twitter API instance is not initialized with client ID. ' + + 'Twitter API instance is not initialized with client ID. You can find your client ID in Twitter Developer Portal. ' + 'Please build an instance with: new TwitterApi({ clientId: \'\' })', ); } @@ -218,6 +223,8 @@ export default class TwitterApiReadOnly extends TwitterApiBase { * After user is redirect from your callback, use obtained code to * instanciate the new TwitterApi instance. * + * You need to obtain `codeVerifier` from a call to `.generateOAuth2AuthLink`. + * * ```ts * // Use the saved codeVerifier associated to state (present in query string of callback) * const requestClient = new TwitterApi({ clientId: 'yourClientId' }); @@ -248,6 +255,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { redirect_uri: redirectUri, grant_type: 'authorization_code', client_id: this._clientId, + client_secret: this._clientSecret, }); return this.parseOAuth2AccessTokenResult(accessTokenResult); @@ -275,6 +283,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { refresh_token: refreshToken, grant_type: 'refresh_token', client_id: this._clientId, + client_secret: this._clientSecret, }); return this.parseOAuth2AccessTokenResult(accessTokenResult); @@ -296,6 +305,7 @@ export default class TwitterApiReadOnly extends TwitterApiBase { return await this.post('https://api.twitter.com/2/oauth2/revoke', { client_id: this._clientId, + client_secret: this._clientSecret, token, token_type_hint: tokenType, }); diff --git a/src/types/auth.types.ts b/src/types/auth.types.ts index 81d3ccf..7e793e2 100644 --- a/src/types/auth.types.ts +++ b/src/types/auth.types.ts @@ -1,8 +1,9 @@ import type TwitterApi from '../client'; import { TypeOrArrayOf } from './shared.types'; -export type TOAuth2Scope = 'tweet.read' | 'users.read' | 'account.follows.read' | 'account.follows.write' - | 'offline.access' | 'space.read'; +export type TOAuth2Scope = 'tweet.read' | 'tweet.write' | 'tweet.moderate.write' | 'users.read' | 'follows.read' | 'follows.write' + | 'offline.access' | 'space.read' | 'mute.read' | 'mute.write' | 'like.read' | 'like.write' | 'list.read' | 'list.write' + | 'block.read' | 'block.write'; export interface BuildOAuth2RequestLinkArgs { scope?: TypeOrArrayOf | TypeOrArrayOf; diff --git a/src/types/client.types.ts b/src/types/client.types.ts index f98afca..6db4444 100644 --- a/src/types/client.types.ts +++ b/src/types/client.types.ts @@ -24,6 +24,7 @@ export interface TwitterApiTokens { export interface TwitterApiOAuth2Init { clientId: string; + clientSecret?: string; } export interface TwitterApiBasicAuth { diff --git a/src/types/errors.types.ts b/src/types/errors.types.ts index 5990a69..b197bd4 100644 --- a/src/types/errors.types.ts +++ b/src/types/errors.types.ts @@ -206,7 +206,7 @@ export enum EApiV1ErrorCode { InvalidCoordinates = 3, NoLocationFound = 13, - // Authentification failures + // Authentication failures AuthenticationFail = 32, InvalidOrExpiredToken = 89, UnableToVerifyCredentials = 99, diff --git a/src/v2/client.v2.read.ts b/src/v2/client.v2.read.ts index e861b41..ba86fbe 100644 --- a/src/v2/client.v2.read.ts +++ b/src/v2/client.v2.read.ts @@ -212,6 +212,16 @@ export default class TwitterApiv2ReadOnly extends TwitterApiSubClient { /* Users */ + /** + * Returns information about an authorized user. + * https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me + * + * OAuth2 scopes: `tweet.read` & `users.read` + */ + public me(options: Partial = {}) { + return this.get('users/me', options); + } + /** * Returns a variety of information about a single user specified by the requested ID. * https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-id @@ -277,7 +287,7 @@ export default class TwitterApiv2ReadOnly extends TwitterApiSubClient { * Returns a list of users the specified user ID is following. * https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/get-users-id-following * - * OAuth2 scope: `account.follows.read` + * OAuth2 scope: `follows.read` */ public following(userId: string, options?: Partial): Promise; public following(userId: string, options: FollowersV2ParamsWithPaginator): Promise; diff --git a/src/v2/client.v2.write.ts b/src/v2/client.v2.write.ts index 9234e51..0570bae 100644 --- a/src/v2/client.v2.write.ts +++ b/src/v2/client.v2.write.ts @@ -174,7 +174,7 @@ export default class TwitterApiv2ReadWrite extends TwitterApiv2ReadOnly { * If the target user does not have public Tweets, this endpoint will send a follow request. * https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/post-users-source_user_id-following * - * OAuth2 scope: `account.follows.write` + * OAuth2 scope: `follows.write` * * **Note**: You must specify the currently logged user ID ; you can obtain it through v1.1 API. */ @@ -186,7 +186,7 @@ export default class TwitterApiv2ReadWrite extends TwitterApiv2ReadOnly { * Allows a user ID to unfollow another user. * https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/delete-users-source_id-following * - * OAuth2 scope: `account.follows.write` + * OAuth2 scope: `follows.write` * * **Note**: You must specify the currently logged user ID ; you can obtain it through v1.1 API. */ diff --git a/test/auth.test.ts b/test/auth.test.ts index 696963e..fe316bd 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -5,7 +5,7 @@ import { getRequestClient } from '../src/test/utils'; // OAuth 1.0a const clientWithoutUser = getRequestClient(); -describe('Authentification API', () => { +describe('Authentication API', () => { it('.generateAuthLink - Create a auth link', async () => { const tokens = await clientWithoutUser.generateAuthLink('oob'); diff --git a/test/list.v1.test.ts b/test/list.v1.test.ts index c3d150f..db97150 100644 --- a/test/list.v1.test.ts +++ b/test/list.v1.test.ts @@ -12,18 +12,23 @@ describe('List endpoints for v1.1 API', () => { it('.createList/.updateList/.listOwnerships/.removeList/.list - Create, update, get and delete a list', async () => { const newList = await client.v1.createList({ name: 'cats', mode: 'private' }); + + await sleepTest(1000); let createdList = await client.v1.list({ list_id: newList.id_str }); expect(createdList.id_str).to.equal(newList.id_str); await client.v1.updateList({ list_id: newList.id_str, name: 'cats updated' }); + await sleepTest(1000); createdList = await client.v1.list({ list_id: newList.id_str }); expect(createdList.name).to.equal('cats updated'); const ownerships = await client.v1.listOwnerships(); expect(ownerships.lists.some(l => l.id_str === newList.id_str)).to.equal(true); - await client.v1.removeList({ list_id: newList.id_str }); + await sleepTest(1000); + // This {does} works, but sometimes a 404 is returned... + await client.v1.removeList({ list_id: newList.id_str }).catch(() => {}); }).timeout(60 * 1000); it('.addListMembers/.removeListMembers/.listMembers/.listStatuses - Manage list members and list statuses', async () => { @@ -41,6 +46,7 @@ describe('List endpoints for v1.1 API', () => { await client.v1.removeListMembers({ list_id: newList.id_str, user_id: '12' }); - await client.v1.removeList({ list_id: newList.id_str }); + await sleepTest(1000); + await client.v1.removeList({ list_id: newList.id_str }).catch(() => {}); }).timeout(60 * 1000); });