2022-10-07 15:45:45 -04:00

392 lines
16 KiB
JavaScript

"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;