From fac3cffe1b38af066db7ab4f9a1774734db6b928 Mon Sep 17 00:00:00 2001 From: Fernando Figueroa Date: Thu, 1 Jan 2026 16:32:17 -0300 Subject: [PATCH] feat(audio): add waveform visualization for PTT voice messages - Add audio-decode library for audio buffer analysis - Implement getAudioDuration() to extract duration from audio - Implement getAudioWaveform() to generate 64-value waveform array - Normalize waveform values to 0-100 range for WhatsApp compatibility - Change audio bitrate from 128k to 48k per WhatsApp PTT requirements - Add Baileys patch to prevent waveform overwrite - Increase Node.js heap size for build to prevent OOM Fixes #1086 --- Dockerfile | 5 +- package-lock.json | 261 ++++++++++++++++++ package.json | 2 + patches/baileys+7.0.0-rc.6.patch | 13 + .../whatsapp/whatsapp.baileys.service.ts | 92 +++++- 5 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 patches/baileys+7.0.0-rc.6.patch diff --git a/Dockerfile b/Dockerfile index 24c4e3bc7..747e7d730 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,12 @@ WORKDIR /evolution COPY ./package*.json ./ COPY ./tsconfig.json ./ COPY ./tsup.config.ts ./ +COPY ./patches ./patches RUN npm ci --silent +RUN npx patch-package + COPY ./src ./src COPY ./public ./public COPY ./prisma ./prisma @@ -28,7 +31,7 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* RUN ./Docker/scripts/generate_database.sh -RUN npm run build +RUN NODE_OPTIONS="--max-old-space-size=2048" npm run build FROM node:24-alpine AS final diff --git a/package-lock.json b/package-lock.json index c9c50513a..476a88c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", @@ -5407,6 +5408,13 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -6338,6 +6346,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -8780,6 +8804,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/findup-sync": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", @@ -10054,6 +10088,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10397,6 +10447,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10534,6 +10597,26 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -10567,6 +10650,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -10664,6 +10757,16 @@ "@keyv/serialize": "^1.1.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -12216,6 +12319,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -12624,6 +12744,137 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -14307,6 +14558,16 @@ "url": "https://github.com/sponsors/eshaz" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", diff --git a/package.json b/package.json index 009782b7c..ce83b476f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"", "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"", "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"", + "postinstall": "patch-package", "prepare": "husky" }, "repository": { @@ -147,6 +148,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "patch-package": "^8.0.1", "prettier": "^3.4.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.20.5", diff --git a/patches/baileys+7.0.0-rc.6.patch b/patches/baileys+7.0.0-rc.6.patch new file mode 100644 index 000000000..e01f5cac3 --- /dev/null +++ b/patches/baileys+7.0.0-rc.6.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/baileys/lib/Utils/messages.js b/node_modules/baileys/lib/Utils/messages.js +index 17b05b8..782efb4 100644 +--- a/node_modules/baileys/lib/Utils/messages.js ++++ b/node_modules/baileys/lib/Utils/messages.js +@@ -132,7 +132,7 @@ export const prepareWAMessageMedia = async (message, options) => { + } + const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'; + const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'; +- const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true; ++ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined'; + const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true; + const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation; + const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, { diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 1e3bdcf13..a4b6cba9b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -90,6 +90,7 @@ import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import axios from 'axios'; +import audioDecode from 'audio-decode'; import makeWASocket, { AnyMessageContent, BufferedEventData, @@ -3006,7 +3007,7 @@ export class BaileysStartupService extends ChannelStartupService { .noVideo() .audioCodec('libopus') .addOutputOptions('-avoid_negative_ts make_zero') - .audioBitrate('128k') + .audioBitrate('48k') .audioFrequency(48000) .audioChannels(1) .outputOptions([ @@ -3038,6 +3039,71 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async getAudioDuration(audioBuffer: Buffer): Promise { + try { + this.logger.info('Getting audio duration...'); + const audioData = await audioDecode(audioBuffer); + const duration = Math.ceil(audioData.duration); + this.logger.info(`Audio duration: ${duration} seconds`); + return duration; + } catch (error) { + this.logger.warn(`Failed to get audio duration: ${error.message}, using default 1 second`); + return 1; + } + } + + private async getAudioWaveform(audioBuffer: Buffer): Promise { + try { + this.logger.info('Generating audio waveform...'); + const audioData = await audioDecode(audioBuffer); + const samples = audioData.getChannelData(0); // Get first channel + const waveformLength = 64; + const samplesPerWaveform = Math.floor(samples.length / waveformLength); + + // First pass: calculate raw averages + const rawValues: number[] = []; + for (let i = 0; i < waveformLength; i++) { + const start = i * samplesPerWaveform; + const end = start + samplesPerWaveform; + let sum = 0; + for (let j = start; j < end && j < samples.length; j++) { + sum += Math.abs(samples[j]); + } + const avg = sum / samplesPerWaveform; + rawValues.push(avg); + } + + // Find max value for normalization + const maxValue = Math.max(...rawValues); + this.logger.info(`Raw waveform max value: ${maxValue}`); + + // Second pass: normalize to 0-100 range + const waveform = new Uint8Array(waveformLength); + if (maxValue > 0) { + for (let i = 0; i < waveformLength; i++) { + // Normalize: (value / max) * 100, ensure minimum of 5 for non-zero values + const normalized = Math.floor((rawValues[i] / maxValue) * 100); + waveform[i] = rawValues[i] > 0 ? Math.max(5, Math.min(100, normalized)) : 0; + } + } else { + // If all values are zero, create a flat waveform at 50 + waveform.fill(50); + } + + // Log first 10 values for debugging + const firstValues = Array.from(waveform.slice(0, 10)); + this.logger.info(`Generated waveform with ${waveform.length} values. First 10: [${firstValues.join(', ')}]`); + this.logger.info(`Waveform type: ${waveform.constructor.name}, isUint8Array: ${waveform instanceof Uint8Array}`); + + return waveform; + } catch (error) { + this.logger.warn(`Failed to generate waveform: ${error.message}, using default`); + const defaultWaveform = new Uint8Array(64); + defaultWaveform.fill(50); + return defaultWaveform; + } + } + public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { const mediaData: SendAudioDto = { ...data }; @@ -3056,9 +3122,17 @@ export class BaileysStartupService extends ChannelStartupService { const convert = await this.processAudio(mediaData.audio); if (Buffer.isBuffer(convert)) { + const seconds = await this.getAudioDuration(convert); + const waveform = await this.getAudioWaveform(convert); + + this.logger.info(`Sending audio with waveform - seconds: ${seconds}, waveform length: ${waveform.length}, waveform type: ${waveform.constructor.name}, isUint8Array: ${waveform instanceof Uint8Array}`); + this.logger.info(`First 10 waveform values: [${Array.from(waveform.slice(0, 10)).join(', ')}]`); + + const messageContent = { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus', seconds, waveform }; + const result = this.sendMessageWithTyping( data.number, - { audio: convert, ptt: true, mimetype: 'audio/ogg; codecs=opus' }, + messageContent as any, { presence: 'recording', delay: data?.delay }, isIntegration, ); @@ -3069,12 +3143,24 @@ export class BaileysStartupService extends ChannelStartupService { } } + const audioBuffer = isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'); + let seconds: number | undefined; + let waveform: Uint8Array | undefined; + + // Only generate waveform for buffers, not URLs + if (Buffer.isBuffer(audioBuffer)) { + seconds = await this.getAudioDuration(audioBuffer); + waveform = await this.getAudioWaveform(audioBuffer); + } + return await this.sendMessageWithTyping( data.number, { - audio: isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'), + audio: audioBuffer, ptt: true, mimetype: 'audio/ogg; codecs=opus', + ...(seconds !== undefined && { seconds }), + ...(waveform !== undefined && { waveform }), }, { presence: 'recording', delay: data?.delay }, isIntegration,