Add files via upload

This commit is contained in:
shahramdj
2022-10-07 15:45:45 -04:00
committed by GitHub
parent 8ddcc312b7
commit 91c72d107e
78 changed files with 6467 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
/// <reference types="node" />
declare type TStringable = {
toString(): string;
};
export declare class FormDataHelper {
protected _boundary: string;
protected _chunks: Buffer[];
protected _footerChunk?: Buffer;
protected static readonly LINE_BREAK = "\r\n";
protected static readonly DEFAULT_CONTENT_TYPE = "application/octet-stream";
protected bodyAppend(...values: (Buffer | string)[]): void;
append(field: string, value: Buffer | string | TStringable, contentType?: string): void;
getHeaders(): {
'content-type': string;
};
/** Length of form-data (including footer length). */
protected getLength(): number;
getBuffer(): Buffer;
protected getBoundary(): string;
protected generateBoundary(): void;
protected getMultipartHeader(field: string, value: string | Buffer, contentType?: string): string;
protected getMultipartFooter(): Buffer;
}
export {};

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FormDataHelper = void 0;
const helpers_1 = require("../helpers");
// This class is partially inspired by https://github.com/form-data/form-data/blob/master/lib/form_data.js
// All credits to their authors.
class FormDataHelper {
constructor() {
this._boundary = '';
this._chunks = [];
}
bodyAppend(...values) {
const allAsBuffer = values.map(val => val instanceof Buffer ? val : Buffer.from(val));
this._chunks.push(...allAsBuffer);
}
append(field, value, contentType) {
const convertedValue = value instanceof Buffer ? value : value.toString();
const header = this.getMultipartHeader(field, convertedValue, contentType);
this.bodyAppend(header, convertedValue, FormDataHelper.LINE_BREAK);
}
getHeaders() {
return {
'content-type': 'multipart/form-data; boundary=' + this.getBoundary(),
};
}
/** Length of form-data (including footer length). */
getLength() {
return this._chunks.reduce((acc, cur) => acc + cur.length, this.getMultipartFooter().length);
}
getBuffer() {
const allChunks = [...this._chunks, this.getMultipartFooter()];
const totalBuffer = Buffer.alloc(this.getLength());
let i = 0;
for (const chunk of allChunks) {
for (let j = 0; j < chunk.length; i++, j++) {
totalBuffer[i] = chunk[j];
}
}
return totalBuffer;
}
getBoundary() {
if (!this._boundary) {
this.generateBoundary();
}
return this._boundary;
}
generateBoundary() {
// This generates a 50 character boundary similar to those used by Firefox.
let boundary = '--------------------------';
for (let i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16);
}
this._boundary = boundary;
}
getMultipartHeader(field, value, contentType) {
// In this lib no need to guess more the content type, octet stream is ok of buffers
if (!contentType) {
contentType = value instanceof Buffer ? FormDataHelper.DEFAULT_CONTENT_TYPE : '';
}
const headers = {
'Content-Disposition': ['form-data', `name="${field}"`],
'Content-Type': contentType,
};
let contents = '';
for (const [prop, header] of Object.entries(headers)) {
// skip nullish headers.
if (!header.length) {
continue;
}
contents += prop + ': ' + (0, helpers_1.arrayWrap)(header).join('; ') + FormDataHelper.LINE_BREAK;
}
return '--' + this.getBoundary() + FormDataHelper.LINE_BREAK + contents + FormDataHelper.LINE_BREAK;
}
getMultipartFooter() {
if (this._footerChunk) {
return this._footerChunk;
}
return this._footerChunk = Buffer.from('--' + this.getBoundary() + '--' + FormDataHelper.LINE_BREAK);
}
}
exports.FormDataHelper = FormDataHelper;
FormDataHelper.LINE_BREAK = '\r\n';
FormDataHelper.DEFAULT_CONTENT_TYPE = 'application/octet-stream';

View File

@@ -0,0 +1,40 @@
export interface OAuth1Tokens {
key: string;
secret: string;
}
export interface OAuth1MakerArgs {
consumerKeys: OAuth1Tokens;
}
export interface OAuth1RequestOptions {
url: string;
method: string;
data?: any;
}
export interface OAuth1AuthInfo {
oauth_consumer_key: string;
oauth_nonce: string;
oauth_signature_method: string;
oauth_timestamp: number;
oauth_version: string;
oauth_token: string;
oauth_signature: string;
}
export declare class OAuth1Helper {
nonceLength: number;
protected consumerKeys: OAuth1Tokens;
constructor(options: OAuth1MakerArgs);
static percentEncode(str: string): string;
protected hash(base: string, key: string): string;
authorize(request: OAuth1RequestOptions, accessTokens?: Partial<OAuth1Tokens>): OAuth1AuthInfo;
toHeader(oauthInfo: OAuth1AuthInfo): {
Authorization: string;
};
protected getNonce(): string;
protected getTimestamp(): number;
protected getSignature(request: OAuth1RequestOptions, tokenSecret: string | undefined, oauthInfo: OAuth1AuthInfo): string;
protected getSigningKey(tokenSecret: string | undefined): string;
protected getBaseString(request: OAuth1RequestOptions, oauthInfo: OAuth1AuthInfo): string;
protected getParameterString(request: OAuth1RequestOptions, oauthInfo: OAuth1AuthInfo): string;
protected getBaseUrl(url: string): string;
}
export default OAuth1Helper;

View File

@@ -0,0 +1,187 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuth1Helper = void 0;
const crypto = __importStar(require("crypto"));
class OAuth1Helper {
constructor(options) {
this.nonceLength = 32;
this.consumerKeys = options.consumerKeys;
}
static percentEncode(str) {
return encodeURIComponent(str)
.replace(/!/g, '%21')
.replace(/\*/g, '%2A')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29');
}
hash(base, key) {
return crypto
.createHmac('sha1', key)
.update(base)
.digest('base64');
}
authorize(request, accessTokens = {}) {
const oauthInfo = {
oauth_consumer_key: this.consumerKeys.key,
oauth_nonce: this.getNonce(),
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: this.getTimestamp(),
oauth_version: '1.0',
};
if (accessTokens.key !== undefined) {
oauthInfo.oauth_token = accessTokens.key;
}
if (!request.data) {
request.data = {};
}
oauthInfo.oauth_signature = this.getSignature(request, accessTokens.secret, oauthInfo);
return oauthInfo;
}
toHeader(oauthInfo) {
const sorted = sortObject(oauthInfo);
let header_value = 'OAuth ';
for (const element of sorted) {
if (element.key.indexOf('oauth_') !== 0) {
continue;
}
header_value += OAuth1Helper.percentEncode(element.key) + '="' + OAuth1Helper.percentEncode(element.value) + '",';
}
return {
// Remove the last ,
Authorization: header_value.slice(0, header_value.length - 1),
};
}
getNonce() {
const wordCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < this.nonceLength; i++) {
result += wordCharacters[Math.trunc(Math.random() * wordCharacters.length)];
}
return result;
}
getTimestamp() {
return Math.trunc(new Date().getTime() / 1000);
}
getSignature(request, tokenSecret, oauthInfo) {
return this.hash(this.getBaseString(request, oauthInfo), this.getSigningKey(tokenSecret));
}
getSigningKey(tokenSecret) {
return OAuth1Helper.percentEncode(this.consumerKeys.secret) + '&' + OAuth1Helper.percentEncode(tokenSecret || '');
}
getBaseString(request, oauthInfo) {
return request.method.toUpperCase() + '&'
+ OAuth1Helper.percentEncode(this.getBaseUrl(request.url)) + '&'
+ OAuth1Helper.percentEncode(this.getParameterString(request, oauthInfo));
}
getParameterString(request, oauthInfo) {
const baseStringData = sortObject(percentEncodeData(mergeObject(oauthInfo, mergeObject(request.data, deParamUrl(request.url)))));
let dataStr = '';
for (const { key, value } of baseStringData) {
// check if the value is an array
// this means that this key has multiple values
if (value && Array.isArray(value)) {
// sort the array first
value.sort();
let valString = '';
// serialize all values for this key: e.g. formkey=formvalue1&formkey=formvalue2
value.forEach((item, i) => {
valString += key + '=' + item;
if (i < value.length) {
valString += '&';
}
});
dataStr += valString;
}
else {
dataStr += key + '=' + value + '&';
}
}
// Remove the last character
return dataStr.slice(0, dataStr.length - 1);
}
getBaseUrl(url) {
return url.split('?')[0];
}
}
exports.OAuth1Helper = OAuth1Helper;
exports.default = OAuth1Helper;
// Helper functions //
function mergeObject(obj1, obj2) {
return {
...obj1 || {},
...obj2 || {},
};
}
function sortObject(data) {
return Object.keys(data)
.sort()
.map(key => ({ key, value: data[key] }));
}
function deParam(string) {
const splitted = string.split('&');
const data = {};
for (const coupleKeyValue of splitted) {
const [key, value = ''] = coupleKeyValue.split('=');
// check if the key already exists
// this can occur if the QS part of the url contains duplicate keys like this: ?formkey=formvalue1&formkey=formvalue2
if (data[key]) {
// the key exists already
if (!Array.isArray(data[key])) {
// replace the value with an array containing the already present value
data[key] = [data[key]];
}
// and add the new found value to it
data[key].push(decodeURIComponent(value));
}
else {
// it doesn't exist, just put the found value in the data object
data[key] = decodeURIComponent(value);
}
}
return data;
}
function deParamUrl(url) {
const tmp = url.split('?');
if (tmp.length === 1)
return {};
return deParam(tmp[1]);
}
function percentEncodeData(data) {
const result = {};
for (const key in data) {
let value = data[key];
// check if the value is an array
if (value && Array.isArray(value)) {
value = value.map(v => OAuth1Helper.percentEncode(v));
}
else {
value = OAuth1Helper.percentEncode(value);
}
result[OAuth1Helper.percentEncode(key)] = value;
}
return result;
}

View File

@@ -0,0 +1,7 @@
export declare class OAuth2Helper {
static getCodeVerifier(): string;
static getCodeChallengeFromVerifier(verifier: string): string;
static getAuthHeader(clientId: string, clientSecret: string): string;
static generateRandomString(length: number): string;
private static escapeBase64Url;
}

View File

@@ -0,0 +1,54 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuth2Helper = void 0;
const crypto = __importStar(require("crypto"));
class OAuth2Helper {
static getCodeVerifier() {
return this.generateRandomString(128);
}
static getCodeChallengeFromVerifier(verifier) {
return this.escapeBase64Url(crypto
.createHash('sha256')
.update(verifier)
.digest('base64'));
}
static getAuthHeader(clientId, clientSecret) {
const key = encodeURIComponent(clientId) + ':' + encodeURIComponent(clientSecret);
return Buffer.from(key).toString('base64');
}
static generateRandomString(length) {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
for (let i = 0; i < length; i++) {
text += possible[Math.floor(Math.random() * possible.length)];
}
return text;
}
static escapeBase64Url(string) {
return string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
}
exports.OAuth2Helper = OAuth2Helper;

View File

@@ -0,0 +1,60 @@
/// <reference types="node" />
import type { Socket } from 'net';
import type { IncomingMessage, ClientRequest } from 'http';
import TweetStream from '../stream/TweetStream';
import { ApiPartialResponseError, ApiRequestError, ApiResponseError } from '../types';
import type { ErrorV1, ErrorV2, TwitterRateLimit, TwitterResponse } from '../types';
import type { TRequestFullData, TRequestFullStreamData, TResponseParseMode } from '../types/request-maker.mixin.types';
import * as zlib from 'zlib';
import { Readable } from 'stream';
declare type TRequestReadyPayload = {
req: ClientRequest;
res: Readable;
originalResponse: IncomingMessage;
requestData: TRequestFullData | TRequestFullStreamData;
};
declare type TReadyRequestResolver = (value: TRequestReadyPayload) => void;
declare type TResponseResolver<T> = (value: TwitterResponse<T>) => void;
declare type TRequestRejecter = (error: ApiRequestError) => void;
declare type TResponseRejecter = (error: ApiResponseError | ApiPartialResponseError) => void;
interface IBuildErrorParams {
res: IncomingMessage;
data: any;
rateLimit?: TwitterRateLimit;
code: number;
}
export declare class RequestHandlerHelper<T> {
protected requestData: TRequestFullData | TRequestFullStreamData;
protected req: ClientRequest;
protected res: IncomingMessage;
protected requestErrorHandled: boolean;
protected responseData: Buffer[];
constructor(requestData: TRequestFullData | TRequestFullStreamData);
get hrefPathname(): string;
protected isCompressionDisabled(): boolean;
protected isFormEncodedEndpoint(): boolean;
protected createRequestError(error: Error): ApiRequestError;
protected createPartialResponseError(error: Error, abortClose: boolean): ApiPartialResponseError;
protected formatV1Errors(errors: ErrorV1[]): string;
protected formatV2Error(error: ErrorV2): string;
protected createResponseError({ res, data, rateLimit, code }: IBuildErrorParams): ApiResponseError;
protected getResponseDataStream(res: IncomingMessage): IncomingMessage | zlib.BrotliDecompress;
protected detectResponseType(res: IncomingMessage): TResponseParseMode;
protected getParsedResponse(res: IncomingMessage): any;
protected getRateLimitFromResponse(res: IncomingMessage): TwitterRateLimit | undefined;
protected onSocketEventHandler(reject: TRequestRejecter, socket: Socket): void;
protected onSocketCloseHandler(reject: TRequestRejecter): void;
protected requestErrorHandler(reject: TRequestRejecter, requestError: Error): void;
protected timeoutErrorHandler(): void;
protected classicResponseHandler(resolve: TResponseResolver<T>, reject: TResponseRejecter, res: IncomingMessage): void;
protected onResponseEndHandler(resolve: TResponseResolver<T>, reject: TResponseRejecter): void;
protected onResponseCloseHandler(resolve: TResponseResolver<T>, reject: TResponseRejecter): void;
protected streamResponseHandler(resolve: TReadyRequestResolver, reject: TResponseRejecter, res: IncomingMessage): void;
protected debugRequest(): void;
protected buildRequest(): void;
protected registerRequestEventDebugHandlers(req: ClientRequest): void;
makeRequest(): Promise<TwitterResponse<T>>;
makeRequestAsStream(): Promise<TweetStream<T>>;
makeRequestAndResolveWhenReady(): Promise<TRequestReadyPayload>;
}
export default RequestHandlerHelper;

View File

@@ -0,0 +1,391 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestHandlerHelper = void 0;
const https_1 = require("https");
const settings_1 = require("../settings");
const TweetStream_1 = __importDefault(require("../stream/TweetStream"));
const types_1 = require("../types");
const zlib = __importStar(require("zlib"));
class RequestHandlerHelper {
constructor(requestData) {
this.requestData = requestData;
this.requestErrorHandled = false;
this.responseData = [];
}
/* Request helpers */
get hrefPathname() {
const url = this.requestData.url;
return url.hostname + url.pathname;
}
isCompressionDisabled() {
return !this.requestData.compression || this.requestData.compression === 'identity';
}
isFormEncodedEndpoint() {
return this.requestData.url.href.startsWith('https://api.twitter.com/oauth/');
}
/* Error helpers */
createRequestError(error) {
if (settings_1.TwitterApiV2Settings.debug) {
settings_1.TwitterApiV2Settings.logger.log('Request error:', error);
}
return new types_1.ApiRequestError('Request failed.', {
request: this.req,
error,
});
}
createPartialResponseError(error, abortClose) {
const res = this.res;
let message = `Request failed with partial response with HTTP code ${res.statusCode}`;
if (abortClose) {
message += ' (connection abruptly closed)';
}
else {
message += ' (parse error)';
}
return new types_1.ApiPartialResponseError(message, {
request: this.req,
response: this.res,
responseError: error,
rawContent: Buffer.concat(this.responseData).toString(),
});
}
formatV1Errors(errors) {
return errors
.map(({ code, message }) => `${message} (Twitter code ${code})`)
.join(', ');
}
formatV2Error(error) {
return `${error.title}: ${error.detail} (see ${error.type})`;
}
createResponseError({ res, data, rateLimit, code }) {
var _a;
if (settings_1.TwitterApiV2Settings.debug) {
settings_1.TwitterApiV2Settings.logger.log(`Request failed with code ${code}, data:`, data);
settings_1.TwitterApiV2Settings.logger.log('Response headers:', res.headers);
}
// Errors formatting.
let errorString = `Request failed with code ${code}`;
if ((_a = data === null || data === void 0 ? void 0 : data.errors) === null || _a === void 0 ? void 0 : _a.length) {
const errors = data.errors;
if ('code' in errors[0]) {
errorString += ' - ' + this.formatV1Errors(errors);
}
else {
errorString += ' - ' + this.formatV2Error(data);
}
}
return new types_1.ApiResponseError(errorString, {
code,
data,
headers: res.headers,
request: this.req,
response: res,
rateLimit,
});
}
/* Response helpers */
getResponseDataStream(res) {
if (this.isCompressionDisabled()) {
return res;
}
const contentEncoding = (res.headers['content-encoding'] || 'identity').trim().toLowerCase();
if (contentEncoding === 'br') {
const brotli = zlib.createBrotliDecompress({
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH,
});
res.pipe(brotli);
return brotli;
}
if (contentEncoding === 'gzip') {
const gunzip = zlib.createGunzip({
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH,
});
res.pipe(gunzip);
return gunzip;
}
if (contentEncoding === 'deflate') {
const inflate = zlib.createInflate({
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH,
});
res.pipe(inflate);
return inflate;
}
return res;
}
detectResponseType(res) {
var _a, _b;
// Auto parse if server responds with JSON body
if (((_a = res.headers['content-type']) === null || _a === void 0 ? void 0 : _a.includes('application/json')) || ((_b = res.headers['content-type']) === null || _b === void 0 ? void 0 : _b.includes('application/problem+json'))) {
return 'json';
}
// f-e oauth token endpoints
else if (this.isFormEncodedEndpoint()) {
return 'url';
}
return 'text';
}
getParsedResponse(res) {
const data = this.responseData;
const mode = this.requestData.forceParseMode || this.detectResponseType(res);
if (mode === 'buffer') {
return Buffer.concat(data);
}
else if (mode === 'text') {
return Buffer.concat(data).toString();
}
else if (mode === 'json') {
const asText = Buffer.concat(data).toString();
return asText.length ? JSON.parse(asText) : undefined;
}
else if (mode === 'url') {
const asText = Buffer.concat(data).toString();
const formEntries = {};
for (const [item, value] of new URLSearchParams(asText)) {
formEntries[item] = value;
}
return formEntries;
}
else {
// mode === 'none'
return undefined;
}
}
getRateLimitFromResponse(res) {
let rateLimit = undefined;
if (res.headers['x-rate-limit-limit']) {
rateLimit = {
limit: Number(res.headers['x-rate-limit-limit']),
remaining: Number(res.headers['x-rate-limit-remaining']),
reset: Number(res.headers['x-rate-limit-reset']),
};
if (this.requestData.rateLimitSaver) {
this.requestData.rateLimitSaver(rateLimit);
}
}
return rateLimit;
}
/* Request event handlers */
onSocketEventHandler(reject, socket) {
socket.on('close', this.onSocketCloseHandler.bind(this, reject));
}
onSocketCloseHandler(reject) {
this.req.removeAllListeners('timeout');
const res = this.res;
if (res) {
// Response ok, res.close/res.end can handle request ending
return;
}
if (!this.requestErrorHandled) {
return reject(this.createRequestError(new Error('Socket closed without any information.')));
}
// else: other situation
}
requestErrorHandler(reject, requestError) {
var _a, _b;
(_b = (_a = this.requestData).requestEventDebugHandler) === null || _b === void 0 ? void 0 : _b.call(_a, 'request-error', { requestError });
this.requestErrorHandled = true;
reject(this.createRequestError(requestError));
}
timeoutErrorHandler() {
this.requestErrorHandled = true;
this.req.destroy(new Error('Request timeout.'));
}
/* Response event handlers */
classicResponseHandler(resolve, reject, res) {
this.res = res;
const dataStream = this.getResponseDataStream(res);
// Register the response data
dataStream.on('data', chunk => this.responseData.push(chunk));
dataStream.on('end', this.onResponseEndHandler.bind(this, resolve, reject));
dataStream.on('close', this.onResponseCloseHandler.bind(this, resolve, reject));
// Debug handlers
if (this.requestData.requestEventDebugHandler) {
this.requestData.requestEventDebugHandler('response', { res });
res.on('aborted', error => this.requestData.requestEventDebugHandler('response-aborted', { error }));
res.on('error', error => this.requestData.requestEventDebugHandler('response-error', { error }));
res.on('close', () => this.requestData.requestEventDebugHandler('response-close', { data: this.responseData }));
res.on('end', () => this.requestData.requestEventDebugHandler('response-end'));
}
}
onResponseEndHandler(resolve, reject) {
const rateLimit = this.getRateLimitFromResponse(this.res);
let data;
try {
data = this.getParsedResponse(this.res);
}
catch (e) {
reject(this.createPartialResponseError(e, false));
return;
}
// Handle bad error codes
const code = this.res.statusCode;
if (code >= 400) {
reject(this.createResponseError({ data, res: this.res, rateLimit, code }));
return;
}
if (settings_1.TwitterApiV2Settings.debug) {
settings_1.TwitterApiV2Settings.logger.log(`[${this.requestData.options.method} ${this.hrefPathname}]: Request succeeds with code ${this.res.statusCode}`);
settings_1.TwitterApiV2Settings.logger.log('Response body:', data);
}
resolve({
data,
headers: this.res.headers,
rateLimit,
});
}
onResponseCloseHandler(resolve, reject) {
const res = this.res;
if (res.aborted) {
// Try to parse the request (?)
try {
this.getParsedResponse(this.res);
// Ok, try to resolve normally the request
return this.onResponseEndHandler(resolve, reject);
}
catch (e) {
// Parse error, just drop with content
return reject(this.createPartialResponseError(e, true));
}
}
if (!res.complete) {
return reject(this.createPartialResponseError(new Error('Response has been interrupted before response could be parsed.'), true));
}
// else: end has been called
}
streamResponseHandler(resolve, reject, res) {
const code = res.statusCode;
if (code < 400) {
if (settings_1.TwitterApiV2Settings.debug) {
settings_1.TwitterApiV2Settings.logger.log(`[${this.requestData.options.method} ${this.hrefPathname}]: Request succeeds with code ${res.statusCode} (starting stream)`);
}
const dataStream = this.getResponseDataStream(res);
// HTTP code ok, consume stream
resolve({ req: this.req, res: dataStream, originalResponse: res, requestData: this.requestData });
}
else {
// Handle response normally, can only rejects
this.classicResponseHandler(() => undefined, reject, res);
}
}
/* Wrappers for request lifecycle */
debugRequest() {
const url = this.requestData.url;
settings_1.TwitterApiV2Settings.logger.log(`[${this.requestData.options.method} ${this.hrefPathname}]`, this.requestData.options);
if (url.search) {
settings_1.TwitterApiV2Settings.logger.log('Request parameters:', [...url.searchParams.entries()].map(([key, value]) => `${key}: ${value}`));
}
if (this.requestData.body) {
settings_1.TwitterApiV2Settings.logger.log('Request body:', this.requestData.body);
}
}
buildRequest() {
var _a;
const url = this.requestData.url;
const auth = url.username ? `${url.username}:${url.password}` : undefined;
const headers = (_a = this.requestData.options.headers) !== null && _a !== void 0 ? _a : {};
if (this.requestData.compression === true || this.requestData.compression === 'brotli') {
headers['accept-encoding'] = 'br;q=1.0, gzip;q=0.8, deflate;q=0.5, *;q=0.1';
}
else if (this.requestData.compression === 'gzip') {
headers['accept-encoding'] = 'gzip;q=1, deflate;q=0.5, *;q=0.1';
}
else if (this.requestData.compression === 'deflate') {
headers['accept-encoding'] = 'deflate;q=1, *;q=0.1';
}
if (settings_1.TwitterApiV2Settings.debug) {
this.debugRequest();
}
this.req = (0, https_1.request)({
...this.requestData.options,
// Define URL params manually, addresses dependencies error https://github.com/PLhery/node-twitter-api-v2/issues/94
host: url.hostname,
port: url.port || undefined,
path: url.pathname + url.search,
protocol: url.protocol,
auth,
headers,
});
}
registerRequestEventDebugHandlers(req) {
req.on('close', () => this.requestData.requestEventDebugHandler('close'));
req.on('abort', () => this.requestData.requestEventDebugHandler('abort'));
req.on('socket', socket => {
this.requestData.requestEventDebugHandler('socket', { socket });
socket.on('error', error => this.requestData.requestEventDebugHandler('socket-error', { socket, error }));
socket.on('connect', () => this.requestData.requestEventDebugHandler('socket-connect', { socket }));
socket.on('close', withError => this.requestData.requestEventDebugHandler('socket-close', { socket, withError }));
socket.on('end', () => this.requestData.requestEventDebugHandler('socket-end', { socket }));
socket.on('lookup', (...data) => this.requestData.requestEventDebugHandler('socket-lookup', { socket, data }));
socket.on('timeout', () => this.requestData.requestEventDebugHandler('socket-timeout', { socket }));
});
}
makeRequest() {
this.buildRequest();
return new Promise((resolve, reject) => {
const req = this.req;
// Handle request errors
req.on('error', this.requestErrorHandler.bind(this, reject));
req.on('socket', this.onSocketEventHandler.bind(this, reject));
req.on('response', this.classicResponseHandler.bind(this, resolve, reject));
if (this.requestData.options.timeout) {
req.on('timeout', this.timeoutErrorHandler.bind(this));
}
// Debug handlers
if (this.requestData.requestEventDebugHandler) {
this.registerRequestEventDebugHandlers(req);
}
if (this.requestData.body) {
req.write(this.requestData.body);
}
req.end();
});
}
async makeRequestAsStream() {
const { req, res, requestData, originalResponse } = await this.makeRequestAndResolveWhenReady();
return new TweetStream_1.default(requestData, { req, res, originalResponse });
}
makeRequestAndResolveWhenReady() {
this.buildRequest();
return new Promise((resolve, reject) => {
const req = this.req;
// Handle request errors
req.on('error', this.requestErrorHandler.bind(this, reject));
req.on('response', this.streamResponseHandler.bind(this, resolve, reject));
if (this.requestData.body) {
req.write(this.requestData.body);
}
req.end();
});
}
}
exports.RequestHandlerHelper = RequestHandlerHelper;
exports.default = RequestHandlerHelper;

View File

@@ -0,0 +1,58 @@
/// <reference types="node" />
import { IClientSettings, ITwitterApiClientPlugin, TClientTokens, TwitterApiPluginResponseOverride, TwitterRateLimit, TwitterResponse } from '../types';
import TweetStream from '../stream/TweetStream';
import type { ClientRequestArgs } from 'http';
import OAuth1Helper from './oauth1.helper';
import type { IGetHttpRequestArgs, IGetStreamRequestArgs, IGetStreamRequestArgsAsync, IGetStreamRequestArgsSync, IWriteAuthHeadersArgs, TAcceptedInitToken } from '../types/request-maker.mixin.types';
import { IComputedHttpRequestArgs } from '../types/request-maker.mixin.types';
export declare class ClientRequestMaker {
bearerToken?: string;
consumerToken?: string;
consumerSecret?: string;
accessToken?: string;
accessSecret?: string;
basicToken?: string;
clientId?: string;
clientSecret?: string;
rateLimits: {
[endpoint: string]: TwitterRateLimit;
};
clientSettings: Partial<IClientSettings>;
protected _oauth?: OAuth1Helper;
protected static readonly BODY_METHODS: Set<string>;
constructor(settings?: Partial<IClientSettings>);
/** @deprecated - Switch to `@twitter-api-v2/plugin-rate-limit` */
getRateLimits(): {
[endpoint: string]: TwitterRateLimit;
};
protected saveRateLimit(originalUrl: string, rateLimit: TwitterRateLimit): void;
/** Send a new request and returns a wrapped `Promise<TwitterResponse<T>`. */
send<T = any>(requestParams: IGetHttpRequestArgs): Promise<TwitterResponse<T>>;
/**
* Create a new request, then creates a stream from it as a `TweetStream`.
*
* Request will be sent only if `autoConnect` is not set or `true`: return type will be `Promise<TweetStream>`.
* If `autoConnect` is `false`, a `TweetStream` is directly returned and you should call `stream.connect()` by yourself.
*/
sendStream<T = any>(requestParams: IGetHttpRequestArgs & IGetStreamRequestArgsSync): TweetStream<T>;
sendStream<T = any>(requestParams: IGetHttpRequestArgs & IGetStreamRequestArgsAsync): Promise<TweetStream<T>>;
sendStream<T = any>(requestParams: IGetHttpRequestArgs & IGetStreamRequestArgs): Promise<TweetStream<T>> | TweetStream<T>;
initializeToken(token?: TAcceptedInitToken): void;
getActiveTokens(): TClientTokens;
protected buildOAuth(): OAuth1Helper;
protected getOAuthAccessTokens(): {
key: string;
secret: string;
} | undefined;
getPlugins(): ITwitterApiClientPlugin[];
hasPlugins(): boolean;
applyPluginMethod<K extends keyof ITwitterApiClientPlugin>(method: K, args: Parameters<Required<ITwitterApiClientPlugin>[K]>[0]): Promise<TwitterApiPluginResponseOverride | undefined>;
protected writeAuthHeaders({ headers, bodyInSignature, url, method, query, body }: IWriteAuthHeadersArgs): Record<string, string>;
protected getUrlObjectFromUrlString(url: string): URL;
protected getHttpRequestArgs({ url: stringUrl, method, query: rawQuery, body: rawBody, headers, forceBodyMode, enableAuth, params, }: IGetHttpRequestArgs): IComputedHttpRequestArgs;
protected applyPreRequestConfigHooks(requestParams: IGetHttpRequestArgs): Promise<TwitterResponse<any> | undefined>;
protected applyPreStreamRequestConfigHooks(requestParams: IGetHttpRequestArgs): void;
protected applyPreRequestHooks(requestParams: IGetHttpRequestArgs, computedParams: IComputedHttpRequestArgs, requestOptions: Partial<ClientRequestArgs>): Promise<void>;
protected applyPostRequestHooks(requestParams: IGetHttpRequestArgs, computedParams: IComputedHttpRequestArgs, requestOptions: Partial<ClientRequestArgs>, response: TwitterResponse<any>): Promise<TwitterApiPluginResponseOverride | undefined>;
protected applyResponseErrorHooks(requestParams: IGetHttpRequestArgs, computedParams: IComputedHttpRequestArgs, requestOptions: Partial<ClientRequestArgs>, promise: Promise<TwitterResponse<any>>): Promise<TwitterResponse<any>>;
}

View File

@@ -0,0 +1,323 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientRequestMaker = void 0;
const types_1 = require("../types");
const TweetStream_1 = __importDefault(require("../stream/TweetStream"));
const helpers_1 = require("../plugins/helpers");
const helpers_2 = require("../helpers");
const oauth1_helper_1 = __importDefault(require("./oauth1.helper"));
const request_handler_helper_1 = __importDefault(require("./request-handler.helper"));
const request_param_helper_1 = __importDefault(require("./request-param.helper"));
const oauth2_helper_1 = require("./oauth2.helper");
class ClientRequestMaker {
constructor(settings) {
this.rateLimits = {};
this.clientSettings = {};
if (settings) {
this.clientSettings = settings;
}
}
/** @deprecated - Switch to `@twitter-api-v2/plugin-rate-limit` */
getRateLimits() {
return this.rateLimits;
}
saveRateLimit(originalUrl, rateLimit) {
this.rateLimits[originalUrl] = rateLimit;
}
/** Send a new request and returns a wrapped `Promise<TwitterResponse<T>`. */
async send(requestParams) {
var _a, _b, _c, _d, _e;
// Pre-request config hooks
if ((_a = this.clientSettings.plugins) === null || _a === void 0 ? void 0 : _a.length) {
const possibleResponse = await this.applyPreRequestConfigHooks(requestParams);
if (possibleResponse) {
return possibleResponse;
}
}
const args = this.getHttpRequestArgs(requestParams);
const options = {
method: args.method,
headers: args.headers,
timeout: requestParams.timeout,
agent: this.clientSettings.httpAgent,
};
const enableRateLimitSave = requestParams.enableRateLimitSave !== false;
if (args.body) {
request_param_helper_1.default.setBodyLengthHeader(options, args.body);
}
// Pre-request hooks
if ((_b = this.clientSettings.plugins) === null || _b === void 0 ? void 0 : _b.length) {
await this.applyPreRequestHooks(requestParams, args, options);
}
let request = new request_handler_helper_1.default({
url: args.url,
options,
body: args.body,
rateLimitSaver: enableRateLimitSave ? this.saveRateLimit.bind(this, args.rawUrl) : undefined,
requestEventDebugHandler: requestParams.requestEventDebugHandler,
compression: (_d = (_c = requestParams.compression) !== null && _c !== void 0 ? _c : this.clientSettings.compression) !== null && _d !== void 0 ? _d : true,
forceParseMode: requestParams.forceParseMode,
})
.makeRequest();
if ((0, helpers_1.hasRequestErrorPlugins)(this)) {
request = this.applyResponseErrorHooks(requestParams, args, options, request);
}
const response = await request;
// Post-request hooks
if ((_e = this.clientSettings.plugins) === null || _e === void 0 ? void 0 : _e.length) {
const responseOverride = await this.applyPostRequestHooks(requestParams, args, options, response);
if (responseOverride) {
return responseOverride.value;
}
}
return response;
}
sendStream(requestParams) {
var _a, _b;
// Pre-request hooks
if (this.clientSettings.plugins) {
this.applyPreStreamRequestConfigHooks(requestParams);
}
const args = this.getHttpRequestArgs(requestParams);
const options = {
method: args.method,
headers: args.headers,
agent: this.clientSettings.httpAgent,
};
const enableRateLimitSave = requestParams.enableRateLimitSave !== false;
const enableAutoConnect = requestParams.autoConnect !== false;
if (args.body) {
request_param_helper_1.default.setBodyLengthHeader(options, args.body);
}
const requestData = {
url: args.url,
options,
body: args.body,
rateLimitSaver: enableRateLimitSave ? this.saveRateLimit.bind(this, args.rawUrl) : undefined,
payloadIsError: requestParams.payloadIsError,
compression: (_b = (_a = requestParams.compression) !== null && _a !== void 0 ? _a : this.clientSettings.compression) !== null && _b !== void 0 ? _b : true,
};
const stream = new TweetStream_1.default(requestData);
if (!enableAutoConnect) {
return stream;
}
return stream.connect();
}
/* Token helpers */
initializeToken(token) {
if (typeof token === 'string') {
this.bearerToken = token;
}
else if (typeof token === 'object' && 'appKey' in token) {
this.consumerToken = token.appKey;
this.consumerSecret = token.appSecret;
if (token.accessToken && token.accessSecret) {
this.accessToken = token.accessToken;
this.accessSecret = token.accessSecret;
}
this._oauth = this.buildOAuth();
}
else if (typeof token === 'object' && 'username' in token) {
const key = encodeURIComponent(token.username) + ':' + encodeURIComponent(token.password);
this.basicToken = Buffer.from(key).toString('base64');
}
else if (typeof token === 'object' && 'clientId' in token) {
this.clientId = token.clientId;
this.clientSecret = token.clientSecret;
}
}
getActiveTokens() {
if (this.bearerToken) {
return {
type: 'oauth2',
bearerToken: this.bearerToken,
};
}
else if (this.basicToken) {
return {
type: 'basic',
token: this.basicToken,
};
}
else if (this.consumerSecret && this._oauth) {
return {
type: 'oauth-1.0a',
appKey: this.consumerToken,
appSecret: this.consumerSecret,
accessToken: this.accessToken,
accessSecret: this.accessSecret,
};
}
else if (this.clientId) {
return {
type: 'oauth2-user',
clientId: this.clientId,
};
}
return { type: 'none' };
}
buildOAuth() {
if (!this.consumerSecret || !this.consumerToken)
throw new Error('Invalid consumer tokens');
return new oauth1_helper_1.default({
consumerKeys: { key: this.consumerToken, secret: this.consumerSecret },
});
}
getOAuthAccessTokens() {
if (!this.accessSecret || !this.accessToken)
return;
return {
key: this.accessToken,
secret: this.accessSecret,
};
}
/* Plugin helpers */
getPlugins() {
var _a;
return (_a = this.clientSettings.plugins) !== null && _a !== void 0 ? _a : [];
}
hasPlugins() {
var _a;
return !!((_a = this.clientSettings.plugins) === null || _a === void 0 ? void 0 : _a.length);
}
async applyPluginMethod(method, args) {
var _a;
let returnValue;
for (const plugin of this.getPlugins()) {
const value = await ((_a = plugin[method]) === null || _a === void 0 ? void 0 : _a.call(plugin, args));
if (value && value instanceof types_1.TwitterApiPluginResponseOverride) {
returnValue = value;
}
}
return returnValue;
}
/* Request helpers */
writeAuthHeaders({ headers, bodyInSignature, url, method, query, body }) {
headers = { ...headers };
if (this.bearerToken) {
headers.Authorization = 'Bearer ' + this.bearerToken;
}
else if (this.basicToken) {
// 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 ' + oauth2_helper_1.OAuth2Helper.getAuthHeader(this.clientId, this.clientSecret);
}
else if (this.consumerSecret && this._oauth) {
// Merge query and body
const data = bodyInSignature ? request_param_helper_1.default.mergeQueryAndBodyForOAuth(query, body) : query;
const auth = this._oauth.authorize({
url: url.toString(),
method,
data,
}, this.getOAuthAccessTokens());
headers = { ...headers, ...this._oauth.toHeader(auth) };
}
return headers;
}
getUrlObjectFromUrlString(url) {
// Add protocol to URL if needed
if (!url.startsWith('http')) {
url = 'https://' + url;
}
// Convert URL to object that will receive all URL modifications
return new URL(url);
}
getHttpRequestArgs({ url: stringUrl, method, query: rawQuery = {}, body: rawBody = {}, headers, forceBodyMode, enableAuth, params, }) {
let body = undefined;
method = method.toUpperCase();
headers = headers !== null && headers !== void 0 ? headers : {};
// Add user agent header (Twitter recommends it)
if (!headers['x-user-agent']) {
headers['x-user-agent'] = 'Node.twitter-api-v2';
}
const url = this.getUrlObjectFromUrlString(stringUrl);
// URL without query string to save as endpoint name
const rawUrl = url.origin + url.pathname;
// Apply URL parameters
if (params) {
request_param_helper_1.default.applyRequestParametersToUrl(url, params);
}
// Build a URL without anything in QS, and QSP in query
const query = request_param_helper_1.default.formatQueryToString(rawQuery);
request_param_helper_1.default.moveUrlQueryParamsIntoObject(url, query);
// Delete undefined parameters
if (!(rawBody instanceof Buffer)) {
(0, helpers_2.trimUndefinedProperties)(rawBody);
}
// OAuth signature should not include parameters when using multipart.
const bodyType = forceBodyMode !== null && forceBodyMode !== void 0 ? forceBodyMode : request_param_helper_1.default.autoDetectBodyType(url);
// If undefined or true, enable auth by headers
if (enableAuth !== false) {
// OAuth needs body signature only if body is URL encoded.
const bodyInSignature = ClientRequestMaker.BODY_METHODS.has(method) && bodyType === 'url';
headers = this.writeAuthHeaders({ headers, bodyInSignature, method, query, url, body: rawBody });
}
if (ClientRequestMaker.BODY_METHODS.has(method)) {
body = request_param_helper_1.default.constructBodyParams(rawBody, headers, bodyType) || undefined;
}
request_param_helper_1.default.addQueryParamsToUrl(url, query);
return {
rawUrl,
url,
method,
headers,
body,
};
}
/* Plugin helpers */
async applyPreRequestConfigHooks(requestParams) {
var _a;
const url = this.getUrlObjectFromUrlString(requestParams.url);
for (const plugin of this.getPlugins()) {
const result = await ((_a = plugin.onBeforeRequestConfig) === null || _a === void 0 ? void 0 : _a.call(plugin, {
client: this,
url,
params: requestParams,
}));
if (result) {
return result;
}
}
}
applyPreStreamRequestConfigHooks(requestParams) {
var _a;
const url = this.getUrlObjectFromUrlString(requestParams.url);
for (const plugin of this.getPlugins()) {
(_a = plugin.onBeforeStreamRequestConfig) === null || _a === void 0 ? void 0 : _a.call(plugin, {
client: this,
url,
params: requestParams,
});
}
}
async applyPreRequestHooks(requestParams, computedParams, requestOptions) {
await this.applyPluginMethod('onBeforeRequest', {
client: this,
url: this.getUrlObjectFromUrlString(requestParams.url),
params: requestParams,
computedParams,
requestOptions,
});
}
async applyPostRequestHooks(requestParams, computedParams, requestOptions, response) {
return await this.applyPluginMethod('onAfterRequest', {
client: this,
url: this.getUrlObjectFromUrlString(requestParams.url),
params: requestParams,
computedParams,
requestOptions,
response,
});
}
applyResponseErrorHooks(requestParams, computedParams, requestOptions, promise) {
return promise.catch(helpers_1.applyResponseHooks.bind(this, requestParams, computedParams, requestOptions));
}
}
exports.ClientRequestMaker = ClientRequestMaker;
ClientRequestMaker.BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);

View File

@@ -0,0 +1,20 @@
/// <reference types="node" />
import type { RequestOptions } from 'https';
import type { TBodyMode, TRequestBody, TRequestQuery, TRequestStringQuery } from '../types/request-maker.mixin.types';
export declare class RequestParamHelpers {
static readonly JSON_1_1_ENDPOINTS: Set<string>;
static formatQueryToString(query: TRequestQuery): TRequestStringQuery;
static autoDetectBodyType(url: URL): TBodyMode;
static addQueryParamsToUrl(url: URL, query: TRequestQuery): void;
static constructBodyParams(body: TRequestBody, headers: Record<string, string>, mode: TBodyMode): string | Buffer;
static setBodyLengthHeader(options: RequestOptions, body: string | Buffer): void;
static isOAuthSerializable(item: any): boolean;
static mergeQueryAndBodyForOAuth(query: TRequestQuery, body: TRequestBody): any;
static moveUrlQueryParamsIntoObject(url: URL, query: TRequestQuery): URL;
/**
* Replace URL parameters available in pathname, like `:id`, with data given in `parameters`:
* `https://twitter.com/:id.json` + `{ id: '20' }` => `https://twitter.com/20.json`
*/
static applyRequestParametersToUrl(url: URL, parameters: TRequestQuery): URL;
}
export default RequestParamHelpers;

View File

@@ -0,0 +1,145 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestParamHelpers = void 0;
const form_data_helper_1 = require("./form-data.helper");
const oauth1_helper_1 = __importDefault(require("./oauth1.helper"));
/* Helpers functions that are specific to this class but do not depends on instance */
class RequestParamHelpers {
static formatQueryToString(query) {
const formattedQuery = {};
for (const prop in query) {
if (typeof query[prop] === 'string') {
formattedQuery[prop] = query[prop];
}
else if (typeof query[prop] !== 'undefined') {
formattedQuery[prop] = String(query[prop]);
}
}
return formattedQuery;
}
static autoDetectBodyType(url) {
if (url.pathname.startsWith('/2/') || url.pathname.startsWith('/labs/2/')) {
// oauth2 takes url encoded
if (url.password.startsWith('/2/oauth2')) {
return 'url';
}
// Twitter API v2 has JSON-encoded requests for everything else
return 'json';
}
if (url.hostname === 'upload.twitter.com') {
if (url.pathname === '/1.1/media/upload.json') {
return 'form-data';
}
// json except for media/upload command, that is form-data.
return 'json';
}
const endpoint = url.pathname.split('/1.1/', 2)[1];
if (this.JSON_1_1_ENDPOINTS.has(endpoint)) {
return 'json';
}
return 'url';
}
static addQueryParamsToUrl(url, query) {
const queryEntries = Object.entries(query);
if (queryEntries.length) {
let search = '';
for (const [key, value] of queryEntries) {
search += (search.length ? '&' : '?') + `${oauth1_helper_1.default.percentEncode(key)}=${oauth1_helper_1.default.percentEncode(value)}`;
}
url.search = search;
}
}
static constructBodyParams(body, headers, mode) {
if (body instanceof Buffer) {
return body;
}
if (mode === 'json') {
headers['content-type'] = 'application/json;charset=UTF-8';
return JSON.stringify(body);
}
else if (mode === 'url') {
headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
if (Object.keys(body).length) {
return new URLSearchParams(body)
.toString()
.replace(/\*/g, '%2A'); // URLSearchParams doesnt encode '*', but Twitter wants it encoded.
}
return '';
}
else if (mode === 'raw') {
throw new Error('You can only use raw body mode with Buffers. To give a string, use Buffer.from(str).');
}
else {
const form = new form_data_helper_1.FormDataHelper();
for (const parameter in body) {
form.append(parameter, body[parameter]);
}
const formHeaders = form.getHeaders();
headers['content-type'] = formHeaders['content-type'];
return form.getBuffer();
}
}
static setBodyLengthHeader(options, body) {
var _a;
options.headers = (_a = options.headers) !== null && _a !== void 0 ? _a : {};
if (typeof body === 'string') {
options.headers['content-length'] = Buffer.byteLength(body);
}
else {
options.headers['content-length'] = body.length;
}
}
static isOAuthSerializable(item) {
return !(item instanceof Buffer);
}
static mergeQueryAndBodyForOAuth(query, body) {
const parameters = {};
for (const prop in query) {
parameters[prop] = query[prop];
}
if (this.isOAuthSerializable(body)) {
for (const prop in body) {
const bodyProp = body[prop];
if (this.isOAuthSerializable(bodyProp)) {
parameters[prop] = typeof bodyProp === 'object' && bodyProp !== null && 'toString' in bodyProp
? bodyProp.toString()
: bodyProp;
}
}
}
return parameters;
}
static moveUrlQueryParamsIntoObject(url, query) {
for (const [param, value] of url.searchParams) {
query[param] = value;
}
// Remove the query string
url.search = '';
return url;
}
/**
* Replace URL parameters available in pathname, like `:id`, with data given in `parameters`:
* `https://twitter.com/:id.json` + `{ id: '20' }` => `https://twitter.com/20.json`
*/
static applyRequestParametersToUrl(url, parameters) {
url.pathname = url.pathname.replace(/:([A-Z_-]+)/ig, (fullMatch, paramName) => {
if (parameters[paramName] !== undefined) {
return String(parameters[paramName]);
}
return fullMatch;
});
return url;
}
}
exports.RequestParamHelpers = RequestParamHelpers;
RequestParamHelpers.JSON_1_1_ENDPOINTS = new Set([
'direct_messages/events/new.json',
'direct_messages/welcome_messages/new.json',
'direct_messages/welcome_messages/rules/new.json',
'media/metadata/create.json',
'collections/entries/curate.json',
]);
exports.default = RequestParamHelpers;