Merge branch 'develop' into fix/mysql-compat-lid

This commit is contained in:
Davidson Gomes
2026-02-24 12:05:52 -03:00
committed by GitHub
17 changed files with 606 additions and 67 deletions
+28
View File
@@ -2907,6 +2907,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2928,6 +2929,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
"integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
@@ -2940,6 +2942,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2955,6 +2958,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz",
"integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api-logs": "0.204.0",
"import-in-the-middle": "^1.8.1",
@@ -3362,6 +3366,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -3378,6 +3383,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
@@ -3395,6 +3401,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
"integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=14"
}
@@ -3643,6 +3650,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -4933,6 +4941,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -5108,6 +5117,7 @@
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
@@ -5411,6 +5421,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5758,6 +5769,7 @@
"resolved": "https://registry.npmjs.org/audio-decode/-/audio-decode-2.2.3.tgz",
"integrity": "sha512-Z0lHvMayR/Pad9+O9ddzaBJE0DrhZkQlStrC1RwcAHF3AhQAsdwKHeLGK8fYKyp2DDU6xHxzGb4CLMui12yVrg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@wasm-audio-decoders/flac": "^0.2.4",
"@wasm-audio-decoders/ogg-vorbis": "^0.1.15",
@@ -6746,6 +6758,7 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -7636,6 +7649,7 @@
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -7706,6 +7720,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -7762,6 +7777,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8368,6 +8384,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -10355,6 +10372,7 @@
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz",
"integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "1.6.0",
"@jimp/diff": "1.6.0",
@@ -10585,6 +10603,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz",
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@@ -10680,6 +10699,7 @@
"resolved": "https://registry.npmjs.org/link-preview-js/-/link-preview-js-3.2.0.tgz",
"integrity": "sha512-FvrLltjOPGbTzt+RugbzM7g8XuUNLPO2U/INSLczrYdAA32E7nZVUrVL1gr61DGOArGJA2QkPGMEvNMLLsXREA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cheerio": "1.0.0-rc.11",
"url": "0.11.0"
@@ -12600,6 +12620,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -12909,6 +12930,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12938,6 +12960,7 @@
"integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.19.0",
"@prisma/engines": "6.19.0"
@@ -14029,6 +14052,7 @@
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
@@ -14871,6 +14895,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15059,6 +15084,7 @@
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -15707,6 +15733,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16222,6 +16249,7 @@
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"devOptional": true,
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
+1 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"build": "tsc --noEmit && tsup",
"start": "tsx ./src/main.ts",
"start:prod": "node dist/main",
"start:prod": "node --network-family-autoselection-attempt-timeout=1000 dist/main",
"dev:server": "tsx watch ./src/main.ts",
"test": "tsx watch ./test/all.test.ts",
"lint": "eslint --fix --ext .ts src",
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `Instance` MODIFY `token` VARCHAR(500);
+1 -1
View File
@@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Int
disconnectionObject Json? @db.Json
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Instance" ALTER COLUMN "token" TYPE VARCHAR(500);
+1 -1
View File
@@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Integer
disconnectionObject Json? @db.JsonB
+1 -1
View File
@@ -71,7 +71,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Integer
disconnectionObject Json? @db.JsonB
+13
View File
@@ -1,6 +1,7 @@
import {
ArchiveChatDto,
BlockUserDto,
DecryptPollVoteDto,
DeleteMessage,
getBase64FromMediaMessageDto,
MarkChatUnreadDto,
@@ -113,4 +114,16 @@ export class ChatController {
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
return await this.waMonitor.waInstances[instanceName].blockUser(data);
}
public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) {
const pollCreationMessageKey = {
id: data.message.key.id,
remoteJid: data.remoteJid,
};
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
}
public async fetchChannels({ instanceName }: InstanceDto, query: Query<Contact>) {
return await this.waMonitor.waInstances[instanceName].fetchChannels(query);
}
}
+9
View File
@@ -127,3 +127,12 @@ export class BlockUserDto {
number: string;
status: 'block' | 'unblock';
}
export class DecryptPollVoteDto {
message: {
key: {
id: string;
};
};
remoteJid: string;
}
@@ -67,7 +67,6 @@ import {
Chatwoot,
ConfigService,
configService,
ConfigSessionPhone,
Database,
Log,
Openai,
@@ -124,7 +123,6 @@ import makeWASocket, {
Product,
proto,
UserFacingSocketConfig,
WABrowserDescription,
WAMediaUpload,
WAMessage,
WAMessageKey,
@@ -143,7 +141,6 @@ import Long from 'long';
import mimeTypes from 'mime-types';
import NodeCache from 'node-cache';
import cron from 'node-cron';
import { release } from 'os';
import { join } from 'path';
import P from 'pino';
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
@@ -249,6 +246,7 @@ export class BaileysStartupService extends ChannelStartupService {
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
private endSession = false;
private isDeleting = false; // Flag to prevent reconnection during deletion
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
private eventProcessingQueue: Promise<void> = Promise.resolve();
@@ -265,10 +263,27 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async logoutInstance() {
this.messageProcessor.onDestroy();
await this.client?.logout('Log out instance: ' + this.instanceName);
// Mark instance as deleting to prevent reconnection attempts
this.isDeleting = true;
this.endSession = true;
this.client?.ws?.close();
this.messageProcessor.onDestroy();
if (this.client) {
try {
await this.client.logout('Log out instance: ' + this.instanceName);
} catch (error) {
this.logger.error({ message: 'Error during logout', error });
}
// Improved socket cleanup
try {
this.client.ws?.close();
this.client.end(new Error('Instance logout'));
} catch (error) {
this.logger.error({ message: 'Error during socket cleanup', error });
}
}
const db = this.configService.get<Database>('DATABASE');
const cache = this.configService.get<CacheConf>('CACHE');
@@ -332,6 +347,18 @@ export class BaileysStartupService extends ChannelStartupService {
}
private async connectionUpdate({ qr, connection, lastDisconnect }: Partial<ConnectionState>) {
// Enhanced logging for connection updates
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
this.logger.info({
message: 'Connection update received',
connection,
hasQr: !!qr,
statusCode,
instanceName: this.instance.name,
isDeleting: this.isDeleting,
endSession: this.endSession,
});
if (qr) {
if (this.instance.qrcode.count === this.configService.get<QrCode>('QRCODE').LIMIT) {
this.sendDataWebhook(Events.QRCODE_UPDATED, {
@@ -424,11 +451,29 @@ export class BaileysStartupService extends ChannelStartupService {
}
if (connection === 'close') {
// Check if instance is being deleted or session is ending
if (this.isDeleting || this.endSession) {
this.logger.info('Instance is being deleted/ended, skipping reconnection attempt');
return;
}
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
this.logger.info({
message: 'Connection closed, evaluating reconnection',
statusCode,
shouldReconnect,
instanceName: this.instance.name,
});
if (shouldReconnect) {
await this.connectToWhatsapp(this.phoneNumber);
// Add 3 second delay before reconnection to prevent rapid reconnection loops
this.logger.info('Reconnecting in 3 seconds...');
setTimeout(async () => {
await this.connectToWhatsapp(this.phoneNumber);
}, 3000);
} else {
this.sendDataWebhook(Events.STATUS_INSTANCE, {
instance: this.instance.name,
@@ -591,25 +636,16 @@ export class BaileysStartupService extends ChannelStartupService {
private async createClient(number?: string): Promise<WASocket> {
this.instance.authState = await this.defineAuthState();
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
let browserOptions = {};
if (number || this.phoneNumber) {
if (number) {
this.phoneNumber = number;
this.logger.info(`Phone number: ${number}`);
} else {
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
browserOptions = { browser };
this.logger.info(`Browser: ${browser}`);
}
// Fetch latest WhatsApp Web version automatically
const baileysVersion = await fetchLatestWaWebVersion({});
const version = baileysVersion.version;
const log = `Baileys version: ${version.join('.')}`;
const log = `Baileys version: ${version.join('.')}`;
this.logger.info(log);
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
@@ -617,7 +653,7 @@ export class BaileysStartupService extends ChannelStartupService {
let options;
if (this.localProxy?.enabled) {
this.logger.info('Proxy enabled: ' + this.localProxy?.host);
this.logger.verbose('Proxy enabled');
if (this.localProxy?.host?.includes('proxyscrape')) {
try {
@@ -626,9 +662,10 @@ export class BaileysStartupService extends ChannelStartupService {
const proxyUrls = text.split('\r\n');
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
const proxyUrl = 'http://' + proxyUrls[rand];
this.logger.info('Proxy url: ' + proxyUrl);
options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
} catch {
this.localProxy.enabled = false;
} catch (error) {
this.logger.error(error);
}
} else {
options = {
@@ -662,7 +699,7 @@ export class BaileysStartupService extends ChannelStartupService {
msgRetryCounterCache: this.msgRetryCounterCache,
generateHighQualityLinkPreview: true,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
...browserOptions,
// Removido browserOptions para usar Multi-Device nativo (não WebClient)
markOnlineOnConnect: this.localSettings.alwaysOnline,
retryRequestDelayMs: 350,
maxMsgRetryCount: 4,
@@ -1216,10 +1253,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
const messageRaw = this.prepareMessage(received);
const messageRaw = this.prepareMessage(received) as any;
if (messageRaw.messageType === 'pollUpdateMessage') {
const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey;
const pollCreationKey = (messageRaw.message as any).pollUpdateMessage.pollCreationMessageKey;
const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo;
const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any;
@@ -1228,7 +1265,7 @@ export class BaileysStartupService extends ChannelStartupService {
(pollMessage.message as any).pollCreationMessage?.options ||
(pollMessage.message as any).pollCreationMessageV3?.options ||
[];
const pollVote = messageRaw.message.pollUpdateMessage.vote;
const pollVote = (messageRaw.message as any).pollUpdateMessage.vote;
const voterJid = received.key.fromMe
? this.instance.wuid
@@ -1308,14 +1345,14 @@ export class BaileysStartupService extends ChannelStartupService {
})
.map((option) => option.optionName);
messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames;
(messageRaw.message as any).pollUpdateMessage.vote.selectedOptions = selectedOptionNames;
const pollUpdates = pollOptions.map((option) => ({
name: option.optionName,
voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [],
}));
messageRaw.pollUpdates = pollUpdates;
(messageRaw as any).pollUpdates = pollUpdates;
}
}
@@ -1363,13 +1400,14 @@ export class BaileysStartupService extends ChannelStartupService {
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
(messageRaw.message as any).speechToText =
`[audio] ${await this.openaiService.speechToText(received, this)}`;
}
}
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { pollUpdates, ...messageData } = messageRaw;
const { pollUpdates, ...messageData } = messageRaw as any;
const msg = await this.prismaRepository.message.create({ data: messageData });
const { remoteJid } = received.key;
@@ -1445,7 +1483,7 @@ export class BaileysStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
(messageRaw.message as any).mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
@@ -1467,7 +1505,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
if (buffer) {
messageRaw.message.base64 = buffer.toString('base64');
(messageRaw.message as any).base64 = buffer.toString('base64');
} else {
// retry to download media
const buffer = await downloadMediaMessage(
@@ -1478,7 +1516,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
if (buffer) {
messageRaw.message.base64 = buffer.toString('base64');
(messageRaw.message as any).base64 = buffer.toString('base64');
}
}
} catch (error) {
@@ -1490,8 +1528,8 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose(messageRaw);
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
if ((messageRaw.key as any).remoteJid?.includes('@lid') && (messageRaw.key as any).remoteJidAlt) {
(messageRaw.key as any).remoteJid = (messageRaw.key as any).remoteJidAlt;
}
console.log(messageRaw);
@@ -1499,7 +1537,7 @@ export class BaileysStartupService extends ChannelStartupService {
await chatbotController.emit({
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
remoteJid: messageRaw.key.remoteJid,
remoteJid: (messageRaw.key as any).remoteJid,
msg: messageRaw,
pushName: messageRaw.pushName,
});
@@ -1528,9 +1566,11 @@ export class BaileysStartupService extends ChannelStartupService {
await saveOnWhatsappCache([
{
remoteJid:
messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid,
remoteJidAlt: messageRaw.key.remoteJidAlt,
lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null,
(messageRaw.key as any).addressingMode === 'lid'
? (messageRaw.key as any).remoteJidAlt
: (messageRaw.key as any).remoteJid,
remoteJidAlt: (messageRaw.key as any).remoteJidAlt,
lid: (messageRaw.key as any).addressingMode === 'lid' ? 'lid' : null,
},
]);
}
@@ -1576,7 +1616,18 @@ export class BaileysStartupService extends ChannelStartupService {
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
for await (const { key, update } of args) {
if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) {
const keyAny = key as any;
if (keyAny.remoteJid) {
keyAny.remoteJid = keyAny.remoteJid.replace(/:.*$/, '');
}
if (keyAny.participant) {
keyAny.participant = keyAny.participant.replace(/:.*$/, '');
}
const normalizedRemoteJid = keyAny.remoteJid;
const normalizedParticipant = keyAny.participant;
if (settings?.groupsIgnore && normalizedRemoteJid?.includes('@g.us')) {
continue;
}
@@ -1627,9 +1678,9 @@ export class BaileysStartupService extends ChannelStartupService {
const message: any = {
keyId: key.id,
remoteJid: key?.remoteJid,
remoteJid: normalizedRemoteJid,
fromMe: key.fromMe,
participant: key?.participant,
participant: normalizedParticipant,
status: status[update.status] ?? 'SERVER_ACK',
pollUpdates,
instanceId: this.instanceId,
@@ -1672,9 +1723,24 @@ export class BaileysStartupService extends ChannelStartupService {
findMessage = messages[0] || null;
if (!findMessage?.id) {
this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`);
this.logger.verbose(
`Original message not found for update after ${maxRetries} retries. Skipping. This is expected for protocol messages or ephemeral events not saved to the database. Key: ${JSON.stringify(key)}`,
);
continue;
}
// Sync the incoming key.remoteJid with the stored one.
// This mutation is safe and necessary because Baileys events might use LIDs while we store Phone JIDs (or vice versa).
// Normalizing ensuring downstream logic uses the identifier that exists in our database.
if (findMessage?.key?.remoteJid && key.remoteJid !== findMessage.key.remoteJid) {
key.remoteJid = findMessage.key.remoteJid;
}
if (findMessage?.key?.remoteJid && findMessage.key.remoteJid !== key.remoteJid) {
this.logger.verbose(
`Updating key.remoteJid from ${key.remoteJid} to ${findMessage.key.remoteJid} based on stored message`,
);
key.remoteJid = findMessage.key.remoteJid;
}
message.messageId = findMessage.id;
}
@@ -2448,7 +2514,7 @@ export class BaileysStartupService extends ChannelStartupService {
messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber();
}
const messageRaw = this.prepareMessage(messageSent);
const messageRaw = this.prepareMessage(messageSent) as any;
const isMedia =
messageSent?.message?.imageMessage ||
@@ -2470,14 +2536,15 @@ export class BaileysStartupService extends ChannelStartupService {
);
}
if (this.configService.get<Openai>('OPENAI').ENABLED && messageRaw?.message?.audioMessage) {
if (this.configService.get<Openai>('OPENAI').ENABLED && (messageRaw as any)?.message?.audioMessage) {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: { instanceId: this.instanceId },
include: { OpenaiCreds: true },
});
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`;
(messageRaw.message as any).speechToText =
`[audio] ${await this.openaiService.speechToText(messageRaw, this)}`;
}
}
@@ -3537,9 +3604,24 @@ export class BaileysStartupService extends ChannelStartupService {
users: { number: string; jid: string; name?: string }[];
} = { groups: [], broadcast: [], users: [] };
const onWhatsapp: OnWhatsAppDto[] = [];
data.numbers.forEach((number) => {
const jid = createJid(number);
if (isJidNewsletter(jid)) {
onWhatsapp.push(
new OnWhatsAppDto(
jid,
true, // Newsletters are always valid
number,
undefined, // Can be fetched later if needed
'newsletter', // Indicate it's a newsletter type
),
);
return;
}
if (isJidGroup(jid)) {
jids.groups.push({ number, jid });
} else if (jid === 'status@broadcast') {
@@ -3549,8 +3631,6 @@ export class BaileysStartupService extends ChannelStartupService {
}
});
const onWhatsapp: OnWhatsAppDto[] = [];
// BROADCAST
onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number)));
@@ -4675,26 +4755,28 @@ export class BaileysStartupService extends ChannelStartupService {
return obj;
}
private prepareMessage(message: proto.IWebMessageInfo): any {
const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
const messageRaw = {
key: message.key, // Save key exactly as it comes from Baileys
private prepareMessage(message: WAMessage): Message {
const keyAny = message.key as any;
const messageRaw: any = {
key: {
...message.key,
remoteJid: keyAny.remoteJid?.replace(/:.*$/, ''),
participant: keyAny.participant?.replace(/:.*$/, ''),
},
pushName:
message.pushName ||
(message.key.fromMe
? 'Você'
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
status: status[message.status],
message: this.deserializeMessageBuffers({ ...message.message }),
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),
messageType: contentType || 'unknown',
messageType: getContentType(message.message),
messageTimestamp: Long.isLong(message.messageTimestamp)
? message.messageTimestamp.toNumber()
: (message.messageTimestamp as number),
source: getDevice(keyAny.id),
instanceId: this.instanceId,
source: getDevice(message.key.id),
status: status[message.status],
contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),
};
if (!messageRaw.status && message.key.fromMe === false) {
@@ -4726,6 +4808,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (isJidNewsletter(message.key.remoteJid) && message.key.fromMe) {
messageRaw.status = status[3]; // DELIVERED MESSAGE TO NEWSLETTER CHANNEL
}
return messageRaw;
}
@@ -5223,4 +5309,299 @@ export class BaileysStartupService extends ChannelStartupService {
},
};
}
public async baileysDecryptPollVote(pollCreationMessageKey: proto.IMessageKey) {
try {
this.logger.verbose('Starting poll vote decryption process');
// Buscar a mensagem de criação da enquete
const pollCreationMessage = (await this.getMessage(pollCreationMessageKey, true)) as proto.IWebMessageInfo;
if (!pollCreationMessage) {
throw new NotFoundException('Poll creation message not found');
}
// Extrair opções da enquete
const pollOptions =
(pollCreationMessage.message as any)?.pollCreationMessage?.options ||
(pollCreationMessage.message as any)?.pollCreationMessageV3?.options ||
[];
if (!pollOptions || pollOptions.length === 0) {
throw new NotFoundException('Poll options not found');
}
// Recuperar chave de criptografia
const pollMessageSecret = (await this.getMessage(pollCreationMessageKey)) as any;
let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret;
if (!pollEncKey) {
throw new NotFoundException('Poll encryption key not found');
}
// Normalizar chave de criptografia
if (typeof pollEncKey === 'string') {
pollEncKey = Buffer.from(pollEncKey, 'base64');
} else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) {
pollEncKey = Buffer.from(pollEncKey.data);
}
if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) {
pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64');
}
// Buscar todas as mensagens de atualização de votos
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
messageType: 'pollUpdateMessage',
},
select: {
id: true,
key: true,
message: true,
messageTimestamp: true,
},
});
this.logger.verbose(`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database`);
// Filtrar apenas mensagens relacionadas a esta enquete específica
const pollUpdateMessages = allPollUpdateMessages.filter((msg) => {
const pollUpdate = (msg.message as any)?.pollUpdateMessage;
if (!pollUpdate) return false;
const creationKey = pollUpdate.pollCreationMessageKey;
if (!creationKey) return false;
return (
creationKey.id === pollCreationMessageKey.id &&
jidNormalizedUser(creationKey.remoteJid || '') === jidNormalizedUser(pollCreationMessageKey.remoteJid || '')
);
});
this.logger.verbose(`Filtered to ${pollUpdateMessages.length} matching poll update messages`);
// Preparar candidatos de JID para descriptografia
const creatorCandidates = [
this.instance.wuid,
this.client.user?.lid,
pollCreationMessage.key.participant,
(pollCreationMessage.key as any).participantAlt,
pollCreationMessage.key.remoteJid,
(pollCreationMessage.key as any).remoteJidAlt,
].filter(Boolean);
const uniqueCreators = [...new Set(creatorCandidates.map((id) => jidNormalizedUser(id)))];
// Processar votos
const votesByUser = new Map<string, { timestamp: number; selectedOptions: string[]; voterJid: string }>();
this.logger.verbose(`Processing ${pollUpdateMessages.length} poll update messages for decryption`);
for (const pollUpdateMsg of pollUpdateMessages) {
const pollVote = (pollUpdateMsg.message as any)?.pollUpdateMessage?.vote;
if (!pollVote) continue;
const key = pollUpdateMsg.key as any;
const voterCandidates = [
this.instance.wuid,
this.client.user?.lid,
key.participant,
key.participantAlt,
key.remoteJidAlt,
key.remoteJid,
].filter(Boolean);
const uniqueVoters = [...new Set(voterCandidates.map((id) => jidNormalizedUser(id)))];
let selectedOptionNames: string[] = [];
let successfulVoterJid: string | undefined;
// Verificar se o voto já está descriptografado
if (pollVote.selectedOptions && Array.isArray(pollVote.selectedOptions)) {
const selectedOptions = pollVote.selectedOptions;
this.logger.verbose('Vote already has selectedOptions, checking format');
// Verificar se são strings (já descriptografado) ou buffers (precisa descriptografar)
if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
// Já está descriptografado como nomes de opções
selectedOptionNames = selectedOptions;
successfulVoterJid = uniqueVoters[0];
this.logger.verbose(
`Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`,
);
} else {
// Está como hash, precisa converter para nomes
selectedOptionNames = pollOptions
.filter((option: any) => {
const hash = createHash('sha256').update(option.optionName).digest();
return selectedOptions.some((selected: any) => {
if (Buffer.isBuffer(selected)) {
return Buffer.compare(selected, hash) === 0;
}
return false;
});
})
.map((option: any) => option.optionName);
successfulVoterJid = uniqueVoters[0];
}
} else if (pollVote.encPayload && pollEncKey) {
// Tentar descriptografar
let decryptedVote: any = null;
for (const creator of uniqueCreators) {
for (const voter of uniqueVoters) {
try {
decryptedVote = decryptPollVote(pollVote, {
pollCreatorJid: creator,
pollMsgId: pollCreationMessage.key.id,
pollEncKey,
voterJid: voter,
} as any);
if (decryptedVote) {
successfulVoterJid = voter;
break;
}
} catch {
// Continue tentando outras combinações
}
}
if (decryptedVote) break;
}
if (decryptedVote && decryptedVote.selectedOptions) {
// Converter hashes para nomes de opções
selectedOptionNames = pollOptions
.filter((option: any) => {
const hash = createHash('sha256').update(option.optionName).digest();
return decryptedVote.selectedOptions.some((selected: any) => {
if (Buffer.isBuffer(selected)) {
return Buffer.compare(selected, hash) === 0;
}
return false;
});
})
.map((option: any) => option.optionName);
this.logger.verbose(
`Successfully decrypted vote for voter: ${successfulVoterJid}, creator: ${uniqueCreators[0]}`,
);
} else {
this.logger.warn(`Failed to decrypt vote. Last error: Could not decrypt with any combination`);
continue;
}
} else {
this.logger.warn('Vote has no encPayload and no selectedOptions, skipping');
continue;
}
if (selectedOptionNames.length > 0 && successfulVoterJid) {
const normalizedVoterJid = jidNormalizedUser(successfulVoterJid);
const existingVote = votesByUser.get(normalizedVoterJid);
// Manter apenas o voto mais recente de cada usuário
if (!existingVote || pollUpdateMsg.messageTimestamp > existingVote.timestamp) {
votesByUser.set(normalizedVoterJid, {
timestamp: pollUpdateMsg.messageTimestamp,
selectedOptions: selectedOptionNames,
voterJid: successfulVoterJid,
});
}
}
}
// Agrupar votos por opção
const results: Record<string, { votes: number; voters: string[] }> = {};
// Inicializar todas as opções com zero votos
pollOptions.forEach((option: any) => {
results[option.optionName] = {
votes: 0,
voters: [],
};
});
// Agregar votos
votesByUser.forEach((voteData) => {
voteData.selectedOptions.forEach((optionName) => {
if (results[optionName]) {
results[optionName].votes++;
if (!results[optionName].voters.includes(voteData.voterJid)) {
results[optionName].voters.push(voteData.voterJid);
}
}
});
});
// Obter nome da enquete
const pollName =
(pollCreationMessage.message as any)?.pollCreationMessage?.name ||
(pollCreationMessage.message as any)?.pollCreationMessageV3?.name ||
'Enquete sem nome';
// Calcular total de votos únicos
const totalVotes = votesByUser.size;
return {
poll: {
name: pollName,
totalVotes,
results,
},
};
} catch (error) {
this.logger.error(`Error decrypting poll votes: ${error}`);
throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
}
}
public async fetchChannels(query: Query<Contact>) {
const page = Number((query as any)?.page ?? 1);
const limit = Number((query as any)?.limit ?? (query as any)?.rows ?? 50);
const skip = (page - 1) * limit;
const messages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
AND: [{ key: { path: ['remoteJid'], not: null } }],
},
orderBy: { messageTimestamp: 'desc' },
select: {
key: true,
messageTimestamp: true,
},
});
const channelMap = new Map<string, { remoteJid: string; pushName: undefined; lastMessageTimestamp: number }>();
for (const msg of messages) {
const key = msg.key as any;
const remoteJid = key?.remoteJid as string | undefined;
if (!remoteJid || !isJidNewsletter(remoteJid)) continue;
if (!channelMap.has(remoteJid)) {
channelMap.set(remoteJid, {
remoteJid,
pushName: undefined, // Push name is never stored for channels, so we set it as undefined
lastMessageTimestamp: msg.messageTimestamp,
});
}
}
const allChannels = Array.from(channelMap.values());
const total = allChannels.length;
const pages = Math.ceil(total / limit);
const records = allChannels.slice(skip, skip + limit);
return {
total,
pages,
currentPage: page,
limit,
records,
};
}
}
@@ -124,9 +124,20 @@ export class WebhookController extends EventController implements EventControlle
try {
if (instance?.enabled && regex.test(instance.url)) {
// Add custom headers for better webhook tracking and debugging
const enhancedHeaders = {
...webhookHeaders,
'Content-Type': 'application/json',
'X-Instance-ID': this.monitor.waInstances[instanceName].instanceId,
'X-Instance-Name': instanceName,
'X-Event-Type': event,
'X-Timestamp': Date.now().toString(),
'User-Agent': 'EvolutionAPI-Webhook/2.3.7',
};
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
headers: enhancedHeaders as Record<string, string>,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
+22
View File
@@ -2,6 +2,7 @@ import { RouterBroker } from '@api/abstract/abstract.router';
import {
ArchiveChatDto,
BlockUserDto,
DecryptPollVoteDto,
DeleteMessage,
getBase64FromMediaMessageDto,
MarkChatUnreadDto,
@@ -23,6 +24,7 @@ import {
archiveChatSchema,
blockUserSchema,
contactValidateSchema,
decryptPollVoteSchema,
deleteMessageSchema,
markChatUnreadSchema,
messageUpSchema,
@@ -281,6 +283,26 @@ export class ChatRouter extends RouterBroker {
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('getPollVote'), ...guards, async (req, res) => {
const response = await this.dataValidate<DecryptPollVoteDto>({
request: req,
schema: decryptPollVoteSchema,
ClassRef: DecryptPollVoteDto,
execute: (instance, data) => chatController.decryptPollVote(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('findChannels'), ...guards, async (req, res) => {
const response = await this.dataValidate({
request: req,
schema: contactValidateSchema,
ClassRef: Query<Contact>,
execute: (instance, query) => chatController.fetchChannels(instance, query as any),
});
return res.status(HttpStatus.OK).json(response);
});
}
+5
View File
@@ -313,6 +313,7 @@ export type Webhook = {
};
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
export type ConfigSessionPhone = { CLIENT: string; NAME: string };
export type Baileys = { VERSION?: string };
export type QrCode = { LIMIT: number; COLOR: string };
export type Typebot = { ENABLED: boolean; API_VERSION: string; SEND_MEDIA_BASE64: boolean };
export type Chatwoot = {
@@ -410,6 +411,7 @@ export interface Env {
WEBHOOK: Webhook;
PUSHER: Pusher;
CONFIG_SESSION_PHONE: ConfigSessionPhone;
BAILEYS: Baileys;
QRCODE: QrCode;
TYPEBOT: Typebot;
CHATWOOT: Chatwoot;
@@ -800,6 +802,9 @@ export class ConfigService {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome',
},
BAILEYS: {
VERSION: process.env?.CONFIG_BAILEYS_VERSION,
},
QRCODE: {
LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30,
COLOR: process.env.QRCODE_COLOR || '#198754',
+6 -1
View File
@@ -35,7 +35,12 @@ function formatBRNumber(jid: string) {
export function createJid(number: string): string {
number = number.replace(/:\d+/, '');
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
if (
number.includes('@g.us') ||
number.includes('@s.whatsapp.net') ||
number.includes('@lid') ||
number.includes('@newsletter')
) {
return number;
}
+17
View File
@@ -1,7 +1,24 @@
import axios, { AxiosRequestConfig } from 'axios';
import { fetchLatestBaileysVersion, WAVersion } from 'baileys';
import { Baileys, configService } from '../config/env.config';
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
// Check if manual version is set via configuration
const baileysConfig = configService.get<Baileys>('BAILEYS');
const manualVersion = baileysConfig?.VERSION;
if (manualVersion) {
const versionParts = manualVersion.split('.').map(Number);
if (versionParts.length === 3 && !versionParts.some(isNaN)) {
return {
version: versionParts as WAVersion,
isLatest: false,
isManual: true,
};
}
}
try {
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
...options,
+23 -3
View File
@@ -1,6 +1,7 @@
import { prismaRepository } from '@api/server.module';
import { configService, Database } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Prisma } from '@prisma/client';
import dayjs from 'dayjs';
const logger = new Logger('OnWhatsappCache');
@@ -164,9 +165,28 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
logger.verbose(
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
);
await prismaRepository.isOnWhatsapp.create({
data: dataPayload,
});
try {
await prismaRepository.isOnWhatsapp.create({
data: dataPayload,
});
} catch (error: any) {
// Check for unique constraint violation (Prisma error code P2002)
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002' &&
(error.meta?.target as string[])?.includes('remoteJid')
) {
logger.verbose(
`[saveOnWhatsappCache] Race condition detected for ${remoteJid}, updating existing record instead.`,
);
await prismaRepository.isOnWhatsapp.update({
where: { remoteJid: remoteJid },
data: dataPayload,
});
} else {
throw error;
}
}
}
} catch (e) {
// Loga o erro mas não para a execução dos outros promises
+22
View File
@@ -447,3 +447,25 @@ export const buttonsMessageSchema: JSONSchema7 = {
},
required: ['number'],
};
export const decryptPollVoteSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
message: {
type: 'object',
properties: {
key: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
required: ['key'],
},
remoteJid: { type: 'string' },
},
required: ['message', 'remoteJid'],
};