mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-19 20:02:20 -06:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6efa879081 | ||
|
|
2534ec2307 | ||
|
|
933a28de26 | ||
|
|
b1b07b7e7f | ||
|
|
bb831d590f | ||
|
|
cb41e65e29 | ||
|
|
52a8d9ea71 | ||
|
|
c7b7a9992e | ||
|
|
f46699ef3f | ||
|
|
72b0833ce2 | ||
|
|
2e3c8184ef | ||
|
|
6f2bef678c | ||
|
|
3325044500 | ||
|
|
6ede76f8cc | ||
|
|
2fee5053f3 | ||
|
|
67c4aa640b | ||
|
|
604c9f968b | ||
|
|
076449e5d6 | ||
|
|
5faf3d18d6 | ||
|
|
52fa931140 |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Instance` MODIFY `token` VARCHAR(500);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,3 +127,12 @@ export class BlockUserDto {
|
||||
number: string;
|
||||
status: 'block' | 'unblock';
|
||||
}
|
||||
|
||||
export class DecryptPollVoteDto {
|
||||
message: {
|
||||
key: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
remoteJid: string;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user