UNPKG

twitter-api-v2-patch

Version:

Strongly typed, full-featured, light, versatile yet powerful Twitter API v1.1 and v2 client for Node.js.

442 lines (441 loc) 18 kB
"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")); const events_1 = require("events"); 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.x.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 (res.headers["x-app-limit-24hour-limit"]) { rateLimit.day = { limit: Number(res.headers["x-app-limit-24hour-limit"]), remaining: Number(res.headers["x-app-limit-24hour-remaining"]), reset: Number(res.headers["x-app-limit-24hour-reset"]), }; } if (this.requestData.rateLimitSaver) { this.requestData.rateLimitSaver(rateLimit); } } return rateLimit; } /* Request event handlers */ onSocketEventHandler(reject, cleanupListener, socket) { const onClose = this.onSocketCloseHandler.bind(this, reject); socket.on("close", onClose); cleanupListener.on("complete", () => socket.off("close", onClose)); } 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-patch/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) => { // Hooks to call when promise is fulfulled to cleanup the socket (shared between requests) const resolve = (value) => { cleanupListener.emit("complete"); _resolve(value); }; const reject = (value) => { cleanupListener.emit("complete"); _reject(value); }; const cleanupListener = new events_1.EventEmitter(); const req = this.req; // Handle request errors req.on("error", this.requestErrorHandler.bind(this, reject)); req.on("socket", this.onSocketEventHandler.bind(this, reject, cleanupListener)); 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;