import { FormDataHelper } from './form-data.helper'; import type { RequestOptions } from 'https'; import type { TBodyMode, TRequestBody, TRequestQuery, TRequestStringQuery } from '../types/request-maker.mixin.types'; import OAuth1Helper from './oauth1.helper'; /* Helpers functions that are specific to this class but do not depends on instance */ export class RequestParamHelpers { static readonly 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', ]); static formatQueryToString(query: TRequestQuery) { const formattedQuery: TRequestStringQuery = {}; for (const prop in query) { if (typeof query[prop] === 'string') { formattedQuery[prop] = query[prop] as string; } else if (typeof query[prop] !== 'undefined') { formattedQuery[prop] = String(query[prop]); } } return formattedQuery; } static autoDetectBodyType(url: URL) : TBodyMode { 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: URL, query: TRequestQuery) { const queryEntries = Object.entries(query) as [string, string][]; if (queryEntries.length) { let search = ''; for (const [key, value] of queryEntries) { search += (search.length ? '&' : '?') + `${OAuth1Helper.percentEncode(key)}=${OAuth1Helper.percentEncode(value)}`; } url.search = search; } } static constructBodyParams( body: TRequestBody, headers: Record<string, string>, mode: TBodyMode, ) { 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 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: RequestOptions, body: string | Buffer) { options.headers = options.headers ?? {}; if (typeof body === 'string') { options.headers['content-length'] = Buffer.byteLength(body); } else { options.headers['content-length'] = body.length; } } static isOAuthSerializable(item: any) { return !(item instanceof Buffer); } static mergeQueryAndBodyForOAuth(query: TRequestQuery, body: TRequestBody) { const parameters: any = {}; for (const prop in query) { parameters[prop] = query[prop]; } if (this.isOAuthSerializable(body)) { for (const prop in body) { const bodyProp = (body as any)[prop]; if (this.isOAuthSerializable(bodyProp)) { parameters[prop] = typeof bodyProp === 'object' && bodyProp !== null && 'toString' in bodyProp ? bodyProp.toString() : bodyProp; } } } return parameters; } static moveUrlQueryParamsIntoObject(url: URL, query: TRequestQuery) { 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: URL, parameters: TRequestQuery) { url.pathname = url.pathname.replace(/:([A-Z_-]+)/ig, (fullMatch, paramName: string) => { if (parameters[paramName] !== undefined) { return String(parameters[paramName]); } return fullMatch; }); return url; } } export default RequestParamHelpers;