Compare commits

...

20 Commits

Author SHA1 Message Date
Davidson Gomes
6efa879081 chore: increase token length in Instance model across MySQL, PostgreSQL, and PSQL Bouncer schemas
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
2025-12-16 14:32:26 -03:00
Davidson Gomes
2534ec2307 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2025-12-16 14:31:24 -03:00
Davidson Gomes
933a28de26 feat(baileys): enhance logout process and connection handling
- Introduced a flag to prevent reconnection during instance deletion.
- Improved logging for connection updates and errors during logout.
- Added a delay before reconnection attempts to avoid rapid loops.
- Enhanced webhook headers for better tracking and debugging.
- Updated configuration to support manual Baileys version setting.
2025-12-16 14:18:05 -03:00
Davidson Gomes
b1b07b7e7f Merge pull request #2321 from Vitordotpy/fix/remotejid-normalization-and-cache-race
fix: normalize remoteJid in message updates and handle race condition in contact cache
2025-12-16 12:49:58 -03:00
Vitordotpy
bb831d590f refactor: optimize retry loop and robustify cache error handling 2025-12-16 12:38:47 -03:00
Vitordotpy
cb41e65e29 fix: enhance logging for missing original messages during updates 2025-12-16 11:32:53 -03:00
Vitordotpy
52a8d9ea71 fix: normalize remoteJid in message updates and handle race condition in contact cache 2025-12-16 11:00:11 -03:00
Davidson Gomes
c7b7a9992e Merge pull request #2319 from Vitordotpy/fix/remotejid-wrong-format
Some checks are pending
Check Code Quality / check-lint-and-build (push) Waiting to run
Build Docker image / Build and Deploy (push) Waiting to run
Security Scan / CodeQL Analysis (javascript) (push) Waiting to run
Security Scan / Dependency Review (push) Waiting to run
fix(baileys): normalize remote JIDs for consistent database lookups
2025-12-15 23:17:53 -03:00
Vitordotpy
f46699ef3f fix(baileys): cast messageRaw and its properties to any for type safety 2025-12-15 22:35:07 -03:00
Vitordotpy
72b0833ce2 fix(baileys): cast messageRaw and its properties to any for type safety 2025-12-15 22:26:52 -03:00
Vitordotpy
2e3c8184ef fix(baileys): normalize remote JIDs for consistent database lookups 2025-12-15 21:38:45 -03:00
Davidson Gomes
6f2bef678c fix(chat): clean up code formatting by removing unnecessary blank lines in chat controller
Some checks failed
Check Code Quality / check-lint-and-build (push) Has been cancelled
Build Docker image / Build and Deploy (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
2025-12-12 17:57:44 -03:00
Davidson Gomes
3325044500 Merge pull request #2297 from oriondesign2015/develop
Feature: Endpoint para Descriptografar e Visualizar Votos de Enquetes
2025-12-12 17:55:28 -03:00
Davidson Gomes
6ede76f8cc Merge branch 'develop' into develop 2025-12-12 17:51:51 -03:00
OrionDesign
2fee5053f3 fix(baileys): corrigir declaração de variável error em blocos catch 2025-12-11 17:11:58 -03:00
OrionDesign
67c4aa640b refactor(baileys): atualizar serviço de mensagens e schemas de validação 2025-12-11 17:01:13 -03:00
Davidson Gomes
604c9f968b Merge pull request #2296 from caiobleggi/main
Some checks failed
Build Docker image / Build and Deploy (push) Has been cancelled
Check Code Quality / check-lint-and-build (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
feat(channel): add support for @newsletter in sendMessage and findChannels
2025-12-11 10:08:54 -03:00
OrionDesign
076449e5d6 Refactor DecryptPollVoteDto and schema structure
Updated DecryptPollVoteDto to use a nested message.key structure and moved remoteJid to the top level. Adjusted the controller and validation schema to match the new structure for consistency and clarity.
2025-12-09 17:03:15 -03:00
OrionDesign
5faf3d18d6 Add poll vote decryption endpoint and logic
Introduces a new API endpoint and supporting logic to decrypt WhatsApp poll votes. Adds DecryptPollVoteDto, validation schema, controller method, and service logic to process and aggregate poll vote results based on poll creation message key.
2025-12-09 16:56:46 -03:00
Caio Bleggi
52fa931140 feat(channel): add support for @newsletter in sendMessage and findChannels 2025-12-09 12:03:47 -03:00
16 changed files with 625 additions and 58 deletions

28
package-lock.json generated
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"
},

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `Instance` MODIFY `token` VARCHAR(500);

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

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Instance" ALTER COLUMN "token" TYPE VARCHAR(500);

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

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

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);
}
}

View File

@@ -127,3 +127,12 @@ export class BlockUserDto {
number: string;
status: 'block' | 'unblock';
}
export class DecryptPollVoteDto {
message: {
key: {
id: string;
};
};
remoteJid: string;
}

View File

@@ -249,6 +249,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 +266,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 +350,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 +454,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,10 +639,11 @@ export class BaileysStartupService extends ChannelStartupService {
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}`);
@@ -602,7 +651,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 {
@@ -611,9 +660,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 = {
@@ -1201,10 +1251,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;
@@ -1213,7 +1263,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
@@ -1293,14 +1343,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;
}
}
@@ -1348,13 +1398,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;
@@ -1430,7 +1481,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 });
}
@@ -1452,7 +1503,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(
@@ -1463,7 +1514,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
if (buffer) {
messageRaw.message.base64 = buffer.toString('base64');
(messageRaw.message as any).base64 = buffer.toString('base64');
}
}
} catch (error) {
@@ -1475,8 +1526,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);
@@ -1484,7 +1535,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,
});
@@ -1513,9 +1564,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,
},
]);
}
@@ -1561,7 +1614,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;
}
@@ -1612,9 +1676,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,
@@ -1637,18 +1701,48 @@ export class BaileysStartupService extends ChannelStartupService {
const searchId = originalMessageId || key.id;
const messages = (await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'id' = ${searchId}
LIMIT 1
`) as any[];
findMessage = messages[0] || null;
let retries = 0;
const maxRetries = 3;
const retryDelay = 500; // 500ms delay to avoid blocking for too long
while (retries < maxRetries) {
const messages = (await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${this.instanceId}
AND "key"->>'id' = ${searchId}
LIMIT 1
`) as any[];
findMessage = messages[0] || null;
if (findMessage?.id) {
break;
}
retries++;
if (retries < maxRetries) {
await delay(retryDelay);
}
}
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;
}
@@ -2422,7 +2516,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 ||
@@ -2444,14 +2538,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)}`;
}
}
@@ -3511,9 +3606,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') {
@@ -3523,8 +3633,6 @@ export class BaileysStartupService extends ChannelStartupService {
}
});
const onWhatsapp: OnWhatsAppDto[] = [];
// BROADCAST
onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number)));
@@ -4649,26 +4757,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) {
@@ -4700,6 +4810,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;
}
@@ -5119,4 +5233,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,
};
}
}

View File

@@ -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,
});

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);
});
}

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',

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;
}

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,

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

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'],
};