diff --git a/package-lock.json b/package-lock.json index c45e8fef..1aafeaba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 6a21fa56..27feca83 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -1,6 +1,7 @@ import { ArchiveChatDto, BlockUserDto, + DecryptPollVoteDto, DeleteMessage, getBase64FromMediaMessageDto, MarkChatUnreadDto, @@ -114,6 +115,13 @@ export class ChatController { 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) { return await this.waMonitor.waInstances[instanceName].fetchChannels(query); } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index b11f32b0..aeaab6f8 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -127,3 +127,12 @@ export class BlockUserDto { number: string; status: 'block' | 'unblock'; } + +export class DecryptPollVoteDto { + message: { + key: { + id: string; + }; + }; + remoteJid: string; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index db743fac..aad88a37 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -5136,6 +5136,253 @@ 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(); + + 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 = {}; + + // 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) { const page = Number((query as any)?.page ?? 1); const limit = Number((query as any)?.limit ?? (query as any)?.rows ?? 50); diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index f372950f..d90f9b36 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -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, @@ -282,6 +284,12 @@ 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({ + request: req, + schema: decryptPollVoteSchema, + ClassRef: DecryptPollVoteDto, + execute: (instance, data) => chatController.decryptPollVote(instance, data), .post(this.routerPath('findChannels'), ...guards, async (req, res) => { const response = await this.dataValidate({ request: req, diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index d514c619..6970fd9b 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -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'], +};