From fb12f6d9d31b91ba5409b5145f9a7885bbcd20ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= <55464917+jlucaso1@users.noreply.github.com> Date: Sat, 27 Sep 2025 23:52:43 -0300 Subject: [PATCH] refactor: replace axios with fetch API and update related types (#1666) --- package.json | 1 - src/Socket/messages-send.ts | 4 +- src/Types/Message.ts | 3 +- src/Types/Socket.ts | 5 +-- src/Types/globals.d.ts | 7 ++++ src/Utils/chat-utils.ts | 9 ++--- src/Utils/generics.ts | 30 +++++++++----- src/Utils/history.ts | 5 +-- src/Utils/link-preview.ts | 5 +-- src/Utils/messages-media.ts | 77 +++++++++++++++++++++++------------- src/Utils/messages.ts | 12 +++--- src/Utils/process-message.ts | 3 +- yarn.lock | 60 +--------------------------- 13 files changed, 97 insertions(+), 124 deletions(-) create mode 100644 src/Types/globals.d.ts diff --git a/package.json b/package.json index 9142573b..53328b08 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", - "axios": "^1.6.0", "libsignal": "git+https://github.com/whiskeysockets/libsignal-node", "lru-cache": "^11.1.0", "music-metadata": "^11.7.0", diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 6287fbed..7f3d81f6 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -57,7 +57,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, - options: axiosOptions, + options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, @@ -1077,7 +1077,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3_000, - ...(axiosOptions || {}) + ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 5005b022..bba8a3f5 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -1,4 +1,3 @@ -import type { AxiosRequestConfig } from 'axios' import type { Readable } from 'stream' import type { URL } from 'url' import { proto } from '../../WAProto/index.js' @@ -335,7 +334,7 @@ export type MediaGenerationOptions = { mediaUploadTimeoutMs?: number - options?: AxiosRequestConfig + options?: RequestInit backgroundColor?: string diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index bafbb3c8..985560c0 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -1,4 +1,3 @@ -import type { AxiosRequestConfig } from 'axios' import type { Agent } from 'https' import type { URL } from 'url' import { proto } from '../../WAProto/index.js' @@ -131,8 +130,8 @@ export type SocketConfig = { snapshot: boolean } - /** options for axios */ - options: AxiosRequestConfig<{}> + /** options for HTTP fetch requests */ + options: RequestInit /** * fetch a message from your store * implement this so that messages failed to send diff --git a/src/Types/globals.d.ts b/src/Types/globals.d.ts new file mode 100644 index 00000000..cea3d2f7 --- /dev/null +++ b/src/Types/globals.d.ts @@ -0,0 +1,7 @@ +declare global { + interface RequestInit { + dispatcher?: any + } +} + +export {} diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index b9bbd483..bb1a8d05 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -1,5 +1,4 @@ import { Boom } from '@hapi/boom' -import type { AxiosRequestConfig } from 'axios' import { proto } from '../../WAProto/index.js' import type { BaileysEventEmitter, @@ -303,7 +302,7 @@ export const decodeSyncdPatch = async ( return result } -export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequestConfig<{}>) => { +export const extractSyncdPatches = async (result: BinaryNode, options: RequestInit) => { const syncNode = getBinaryNodeChild(result, 'sync') const collectionNodes = getBinaryNodeChildren(syncNode, 'collection') @@ -355,7 +354,7 @@ export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequ return final } -export const downloadExternalBlob = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => { +export const downloadExternalBlob = async (blob: proto.IExternalBlobReference, options: RequestInit) => { const stream = await downloadContentFromMessage(blob, 'md-app-state', { options }) const bufferArray: Buffer[] = [] for await (const chunk of stream) { @@ -365,7 +364,7 @@ export const downloadExternalBlob = async (blob: proto.IExternalBlobReference, o return Buffer.concat(bufferArray) } -export const downloadExternalPatch = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => { +export const downloadExternalPatch = async (blob: proto.IExternalBlobReference, options: RequestInit) => { const buffer = await downloadExternalBlob(blob, options) const syncData = decodeAndHydrate(proto.SyncdMutations, buffer) return syncData @@ -424,7 +423,7 @@ export const decodePatches = async ( syncds: proto.ISyncdPatch[], initial: LTHashState, getAppStateSyncKey: FetchAppStateSyncKey, - options: AxiosRequestConfig<{}>, + options: RequestInit, minimumVersionNumber?: number, logger?: ILogger, validateMacs = true diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 0c500dc2..26b874a7 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -1,5 +1,4 @@ import { Boom } from '@hapi/boom' -import axios, { type AxiosRequestConfig } from 'axios' import { createHash, randomBytes } from 'crypto' import { proto } from '../../WAProto/index.js' const baileysVersion = [2, 3000, 1023223821] @@ -225,16 +224,21 @@ export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWait * utility that fetches latest baileys version from the master branch. * Use to ensure your WA connection is always on the latest version */ -export const fetchLatestBaileysVersion = async (options: AxiosRequestConfig<{}> = {}) => { +export const fetchLatestBaileysVersion = async (options: RequestInit = {}) => { const URL = 'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/index.ts' try { - const result = await axios.get(URL, { - ...options, - responseType: 'text' + const response = await fetch(URL, { + dispatcher: options.dispatcher, + method: 'GET', + headers: options.headers }) + if (!response.ok) { + throw new Boom(`Failed to fetch latest Baileys version: ${response.statusText}`, { statusCode: response.status }) + } + const text = await response.text() // Extract version from line 7 (const version = [...]) - const lines = result.data.split('\n') + const lines = text.split('\n') const versionLine = lines[6] // Line 7 (0-indexed) const versionMatch = versionLine!.match(/const version = \[(\d+),\s*(\d+),\s*(\d+)\]/) @@ -261,12 +265,18 @@ export const fetchLatestBaileysVersion = async (options: AxiosRequestConfig<{}> * A utility that fetches the latest web version of whatsapp. * Use to ensure your WA connection is always on the latest version */ -export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => { +export const fetchLatestWaWebVersion = async (options: RequestInit = {}) => { try { - const { data } = await axios.get('https://web.whatsapp.com/sw.js', { - ...options, - responseType: 'json' + const response = await fetch('https://web.whatsapp.com/sw.js', { + dispatcher: options.dispatcher, + method: 'GET', + headers: options.headers }) + if (!response.ok) { + throw new Boom(`Failed to fetch sw.js: ${response.statusText}`, { statusCode: response.status }) + } + + const data = await response.text() const regex = /\\?"client_revision\\?":\s*(\d+)/ const match = data.match(regex) diff --git a/src/Utils/history.ts b/src/Utils/history.ts index 7d8df8ca..bfac2ff6 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -1,4 +1,3 @@ -import type { AxiosRequestConfig } from 'axios' import { promisify } from 'util' import { inflate } from 'zlib' import { proto } from '../../WAProto/index.js' @@ -11,7 +10,7 @@ import { decodeAndHydrate } from './proto-utils' const inflatePromise = promisify(inflate) -export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: AxiosRequestConfig<{}>) => { +export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => { const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options }) const bufferArray: Buffer[] = [] for await (const chunk of stream) { @@ -96,7 +95,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => { export const downloadAndProcessHistorySyncNotification = async ( msg: proto.Message.IHistorySyncNotification, - options: AxiosRequestConfig<{}> + options: RequestInit ) => { const historyMsg = await downloadHistory(msg, options) return processHistoryMessage(historyMsg) diff --git a/src/Utils/link-preview.ts b/src/Utils/link-preview.ts index a9577cbd..d786f24d 100644 --- a/src/Utils/link-preview.ts +++ b/src/Utils/link-preview.ts @@ -1,4 +1,3 @@ -import type { AxiosRequestConfig } from 'axios' import type { WAMediaUploadFunction, WAUrlInfo } from '../Types' import type { ILogger } from './logger' import { prepareWAMessageMedia } from './messages' @@ -19,7 +18,7 @@ export type URLGenerationOptions = { /** Timeout in ms */ timeout: number proxyUrl?: string - headers?: AxiosRequestConfig<{}>['headers'] + headers?: HeadersInit } uploadImage?: WAMediaUploadFunction logger?: ILogger @@ -70,7 +69,7 @@ export const getUrlInfo = async ( return false } }, - headers: opts.fetchOpts as {} + headers: opts.fetchOpts?.headers as {} }) if (info && 'title' in info && info.title) { const [image] = info.images diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index 5660e1fa..6adb4b48 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -1,5 +1,4 @@ import { Boom } from '@hapi/boom' -import axios, { type AxiosRequestConfig } from 'axios' import { exec } from 'child_process' import * as Crypto from 'crypto' import { once } from 'events' @@ -301,7 +300,7 @@ export const toBuffer = async (stream: Readable) => { return Buffer.concat(chunks) } -export const getStream = async (item: WAMediaUpload, opts?: AxiosRequestConfig) => { +export const getStream = async (item: WAMediaUpload, opts?: RequestInit & { maxContentLength?: number }) => { if (Buffer.isBuffer(item)) { return { stream: toReadable(item), type: 'buffer' } as const } @@ -362,15 +361,24 @@ export async function generateThumbnail( } } -export const getHttpStream = async (url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => { - const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' }) - return fetched.data as Readable +export const getHttpStream = async (url: string | URL, options: RequestInit & { isStream?: true } = {}) => { + const response = await fetch(url.toString(), { + dispatcher: options.dispatcher, + method: 'GET', + headers: options.headers as HeadersInit + }) + if (!response.ok) { + throw new Boom(`Failed to fetch stream from ${url}`, { statusCode: response.status, data: { url } }) + } + + // @ts-ignore Node18+ Readable.fromWeb exists + return Readable.fromWeb(response.body as any) } type EncryptedStreamOptions = { saveOriginalFileIfRequired?: boolean logger?: ILogger - opts?: AxiosRequestConfig + opts?: RequestInit } export const encryptedStream = async ( @@ -412,7 +420,11 @@ export const encryptedStream = async ( for await (const data of stream) { fileLength += data.length - if (type === 'remote' && opts?.maxContentLength && fileLength + data.length > opts.maxContentLength) { + if ( + type === 'remote' && + (opts as any)?.maxContentLength && + fileLength + data.length > (opts as any).maxContentLength + ) { throw new Boom(`content length exceeded when encrypting "${type}"`, { data: { media, type } }) @@ -486,7 +498,7 @@ const toSmallestChunkSize = (num: number) => { export type MediaDownloadOptions = { startByte?: number endByte?: number - options?: AxiosRequestConfig<{}> + options?: RequestInit } export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}` @@ -532,8 +544,13 @@ export const downloadEncryptedContent = async ( const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined - const headers: AxiosRequestConfig['headers'] = { - ...(options?.headers || {}), + const headersInit = options?.headers ? options.headers : undefined + const headers: Record = { + ...(headersInit + ? Array.isArray(headersInit) + ? Object.fromEntries(headersInit) + : (headersInit as Record) + : {}), Origin: DEFAULT_ORIGIN } if (startChunk || endChunk) { @@ -546,9 +563,7 @@ export const downloadEncryptedContent = async ( // download the message const fetched = await getHttpStream(downloadUrl, { ...(options || {}), - headers, - maxBodyLength: Infinity, - maxContentLength: Infinity + headers }) let remainingBytes = Buffer.from([]) @@ -645,21 +660,31 @@ export const getWAUploadToServer = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any try { - const body = await axios.post(url, createReadStream(filePath), { - ...options, - maxRedirects: 0, + const stream = createReadStream(filePath) + const response = await fetch(url, { + dispatcher: fetchAgent, + method: 'POST', + body: stream as any, headers: { - ...(options.headers || {}), + ...(() => { + const hdrs = options?.headers + if (!hdrs) return {} + return Array.isArray(hdrs) ? Object.fromEntries(hdrs) : (hdrs as Record) + })(), 'Content-Type': 'application/octet-stream', Origin: DEFAULT_ORIGIN }, - httpsAgent: fetchAgent, - timeout: timeoutMs, - responseType: 'json', - maxBodyLength: Infinity, - maxContentLength: Infinity + // Note: custom agents/proxy require undici Agent; omitted here. + signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined }) - result = body.data + let parsed: any = undefined + try { + parsed = await response.json() + } catch { + parsed = undefined + } + + result = parsed if (result?.url || result?.directPath) { urls = { @@ -675,13 +700,9 @@ export const getWAUploadToServer = ( throw new Error(`upload failed, reason: ${JSON.stringify(result)}`) } } catch (error: any) { - if (axios.isAxiosError(error)) { - result = error.response?.data - } - const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname logger.warn( - { trace: error.stack, uploadResult: result }, + { trace: error?.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}` ) } diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index c4357fbf..58b08a85 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -1,5 +1,4 @@ import { Boom } from '@hapi/boom' -import axios from 'axios' import { randomBytes } from 'crypto' import { promises as fs } from 'fs' import { type Transform } from 'stream' @@ -455,9 +454,10 @@ export const generateWAMessageContent = async ( if (options.getProfilePicUrl) { const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview') if (pfpUrl) { - const resp = await axios.get(pfpUrl, { responseType: 'arraybuffer' }) - if (resp.status === 200) { - m.groupInviteMessage.jpegThumbnail = resp.data + const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher }) + if (resp.ok) { + const buf = Buffer.from(await resp.arrayBuffer()) + m.groupInviteMessage.jpegThumbnail = buf } } } @@ -933,8 +933,8 @@ export const downloadMediaMessage = async ( const result = await downloadMsg().catch(async error => { if ( ctx && - axios.isAxiosError(error) && // check if the message requires a reupload - REUPLOAD_REQUIRED_STATUS.includes(error.response?.status!) + typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload + REUPLOAD_REQUIRED_STATUS.includes(error.status as number) ) { ctx.logger.info({ key: message.key }, 'sending reupload media request...') // request reupload diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index 4d8ab548..07a835a5 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -1,4 +1,3 @@ -import type { AxiosRequestConfig } from 'axios' import { proto } from '../../WAProto/index.js' import type { AuthenticationCreds, @@ -28,7 +27,7 @@ type ProcessMessageContext = { keyStore: SignalKeyStoreWithTransaction ev: BaileysEventEmitter logger?: ILogger - options: AxiosRequestConfig<{}> + options: RequestInit signalRepository: SignalRepositoryWithLIDStore } diff --git a/yarn.lock b/yarn.lock index 2edb9b2c..3b474557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2874,13 +2874,6 @@ __metadata: languageName: node linkType: hard -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d - languageName: node - linkType: hard - "atomic-sleep@npm:^1.0.0": version: 1.0.0 resolution: "atomic-sleep@npm:1.0.0" @@ -2904,17 +2897,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.0": - version: 1.10.0 - resolution: "axios@npm:1.10.0" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10c0/2239cb269cc789eac22f5d1aabd58e1a83f8f364c92c2caa97b6f5cbb4ab2903d2e557d9dc670b5813e9bcdebfb149e783fb8ab3e45098635cd2f559b06bd5d8 - languageName: node - linkType: hard - "babel-jest@npm:30.0.5": version: 30.0.5 resolution: "babel-jest@npm:30.0.5" @@ -3008,7 +2990,6 @@ __metadata: "@typescript-eslint/parser": "npm:^8" "@whiskeysockets/eslint-config": "npm:^1.0.0" async-mutex: "npm:^0.5.0" - axios: "npm:^1.6.0" conventional-changelog-cli: "npm:^2.2.2" esbuild-register: "npm:^3.6.0" eslint: "npm:^9" @@ -3590,15 +3571,6 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 - languageName: node - linkType: hard - "compare-func@npm:^2.0.0": version: 2.0.0 resolution: "compare-func@npm:2.0.0" @@ -4137,13 +4109,6 @@ __metadata: languageName: node linkType: hard -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 - languageName: node - linkType: hard - "deprecation@npm:^2.0.0": version: 2.3.1 resolution: "deprecation@npm:2.3.1" @@ -5153,16 +5118,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f - languageName: node - linkType: hard - "for-each@npm:^0.3.3, for-each@npm:^0.3.5": version: 0.3.5 resolution: "for-each@npm:0.3.5" @@ -5189,19 +5144,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695 - languageName: node - linkType: hard - "formdata-polyfill@npm:^4.0.10": version: 4.0.10 resolution: "formdata-polyfill@npm:4.0.10" @@ -7734,7 +7676,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.35, mime-types@npm:^2.1.12": +"mime-types@npm:2.1.35": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: