Merge branch 'develop' into fix/meta-cloud-api-chatbot

This commit is contained in:
Davidson Gomes
2026-02-24 12:18:28 -03:00
committed by GitHub
33 changed files with 2013 additions and 323 deletions
+4 -1
View File
@@ -12,9 +12,12 @@ WORKDIR /evolution
COPY ./package*.json ./
COPY ./tsconfig.json ./
COPY ./tsup.config.ts ./
COPY ./patches ./patches
RUN npm ci --silent
RUN npx patch-package
COPY ./src ./src
COPY ./public ./public
COPY ./prisma ./prisma
@@ -28,7 +31,7 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/*
RUN ./Docker/scripts/generate_database.sh
RUN npm run build
RUN NODE_OPTIONS="--max-old-space-size=2048" npm run build
FROM node:24-alpine AS final
+1
View File
@@ -166,6 +166,7 @@ SQS_ACCESS_KEY_ID=
SQS_SECRET_ACCESS_KEY=
SQS_ACCOUNT_ID=
SQS_REGION=
SQS_BASE_URL=
SQS_MAX_PAYLOAD_SIZE=1048576
# ===========================================
+289
View File
@@ -94,6 +94,7 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"husky": "^9.1.7",
"lint-staged": "^16.1.6",
"patch-package": "^8.0.1",
"prettier": "^3.4.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.5",
@@ -2907,6 +2908,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 +2930,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 +2943,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 +2959,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 +3367,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 +3384,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 +3402,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 +3651,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 +4942,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 +5118,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",
@@ -5365,6 +5376,13 @@
"url": "https://github.com/sponsors/eshaz"
}
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
@@ -5411,6 +5429,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 +5777,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",
@@ -6295,6 +6315,22 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
@@ -6746,6 +6782,7 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -7636,6 +7673,7 @@
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -7706,6 +7744,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 +7801,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8368,6 +8408,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",
@@ -8736,6 +8777,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/findup-sync": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
@@ -9994,6 +10045,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -10337,6 +10404,19 @@
"node": ">=0.10.0"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -10355,6 +10435,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",
@@ -10459,6 +10540,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -10492,6 +10593,16 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"dev": true,
"license": "Public Domain",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
@@ -10585,10 +10696,21 @@
"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"
}
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -10680,6 +10802,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"
@@ -12126,6 +12249,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
@@ -12528,6 +12668,137 @@
"node": ">= 0.8"
}
},
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^10.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.2.4",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/patch-package/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/patch-package/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/patch-package/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/patch-package/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/patch-package/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/patch-package/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/patch-package/node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
@@ -12600,6 +12871,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 +13181,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12938,6 +13211,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 +14303,7 @@
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
@@ -14194,6 +14469,16 @@
"url": "https://github.com/sponsors/eshaz"
}
},
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/slice-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
@@ -14871,6 +15156,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 +15345,7 @@
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -15707,6 +15994,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16222,6 +16510,7 @@
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"devOptional": true,
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
+3 -1
View File
@@ -7,7 +7,7 @@
"scripts": {
"build": "tsc --noEmit && tsup",
"start": "tsx ./src/main.ts",
"start:prod": "node dist/main",
"start:prod": "node --network-family-autoselection-attempt-timeout=1000 dist/main",
"dev:server": "tsx watch ./src/main.ts",
"test": "tsx watch ./test/all.test.ts",
"lint": "eslint --fix --ext .ts src",
@@ -20,6 +20,7 @@
"db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"",
"db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
"postinstall": "patch-package",
"prepare": "husky"
},
"repository": {
@@ -150,6 +151,7 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"husky": "^9.1.7",
"lint-staged": "^16.1.6",
"patch-package": "^8.0.1",
"prettier": "^3.4.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.5",
+13
View File
@@ -0,0 +1,13 @@
diff --git a/node_modules/baileys/lib/Utils/messages.js b/node_modules/baileys/lib/Utils/messages.js
index 17b05b8..782efb4 100644
--- a/node_modules/baileys/lib/Utils/messages.js
+++ b/node_modules/baileys/lib/Utils/messages.js
@@ -132,7 +132,7 @@ export const prepareWAMessageMedia = async (message, options) => {
}
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
- const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
+ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined';
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
@@ -131,8 +131,7 @@ ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRE
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `IsOnWhatsapp` DROP COLUMN `lid`,
MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE `Instance` MODIFY `token` VARCHAR(500);
@@ -0,0 +1,21 @@
-- Re-add lid column that was incorrectly dropped by previous migration
-- This migration ensures backward compatibility for existing installations
-- Check if column exists before adding
SET @dbname = DATABASE();
SET @tablename = 'IsOnWhatsapp';
SET @columnname = 'lid';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(100);')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
+2 -1
View File
@@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Int
disconnectionObject Json? @db.Json
@@ -655,6 +655,7 @@ model IsOnWhatsapp {
id String @id @default(cuid())
remoteJid String @unique @db.VarChar(100)
jidOptions String
lid String? @db.VarChar(100)
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
}
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Instance" ALTER COLUMN "token" TYPE VARCHAR(500);
+1 -1
View File
@@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Integer
disconnectionObject Json? @db.JsonB
+1 -1
View File
@@ -71,7 +71,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.VarChar(500)
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Integer
disconnectionObject Json? @db.JsonB
+13
View File
@@ -1,6 +1,7 @@
import {
ArchiveChatDto,
BlockUserDto,
DecryptPollVoteDto,
DeleteMessage,
getBase64FromMediaMessageDto,
MarkChatUnreadDto,
@@ -113,4 +114,16 @@ export class ChatController {
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
return await this.waMonitor.waInstances[instanceName].blockUser(data);
}
public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) {
const pollCreationMessageKey = {
id: data.message.key.id,
remoteJid: data.remoteJid,
};
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
}
public async fetchChannels({ instanceName }: InstanceDto, query: Query<Contact>) {
return await this.waMonitor.waInstances[instanceName].fetchChannels(query);
}
}
+9
View File
@@ -127,3 +127,12 @@ export class BlockUserDto {
number: string;
status: 'block' | 'unblock';
}
export class DecryptPollVoteDto {
message: {
key: {
id: string;
};
};
remoteJid: string;
}
+2
View File
@@ -14,6 +14,7 @@ export class Options {
mentionsEveryOne?: boolean;
mentioned?: string[];
webhookUrl?: string;
messageId?: string;
}
export class MediaMessage {
@@ -45,6 +46,7 @@ export class Metadata {
mentioned?: string[];
encoding?: boolean;
notConvertSticker?: boolean;
messageId?: string;
}
export class SendTextDto extends Metadata {
@@ -318,7 +318,7 @@ export class EvolutionStartupService extends ChannelStartupService {
let audioFile;
const messageId = v4();
const messageId = options?.messageId ?? v4();
let messageRaw: any;
@@ -548,6 +548,7 @@ export class EvolutionStartupService extends ChannelStartupService {
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
messageId: data?.messageId,
},
null,
isIntegration,
@@ -613,6 +614,7 @@ export class EvolutionStartupService extends ChannelStartupService {
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
messageId: data?.messageId,
},
file,
isIntegration,
@@ -711,6 +713,7 @@ export class EvolutionStartupService extends ChannelStartupService {
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
messageId: data?.messageId,
},
file,
isIntegration,
@@ -736,6 +739,7 @@ export class EvolutionStartupService extends ChannelStartupService {
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
messageId: data?.messageId,
},
null,
isIntegration,
@@ -10,6 +10,12 @@ export class BaileysController {
return instance.baileysOnWhatsapp(body?.jid);
}
public async generateMessageID({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.generateMessageID();
}
public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
@@ -19,6 +19,16 @@ export class BaileysRouter extends RouterBroker {
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('generateMessageID'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.generateMessageID(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
File diff suppressed because it is too large Load Diff
@@ -49,6 +49,14 @@ export class ChatwootService {
private provider: any;
// Cache para deduplicação de orderMessage (evita mensagens duplicadas)
private processedOrderIds: Map<string, number> = new Map();
private readonly ORDER_CACHE_TTL_MS = 30000; // 30 segundos
// Cache para mapeamento LID → Número Normal (resolve problema de @lid)
private lidToPhoneMap: Map<string, { phone: string; timestamp: number }> = new Map();
private readonly LID_CACHE_TTL_MS = 3600000; // 1 hora
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
@@ -632,10 +640,32 @@ export class ChatwootService {
public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.addressingMode === 'lid';
const isGroup = body.key.remoteJid.endsWith('@g.us');
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
const { remoteJid } = body.key;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
let phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
let { remoteJid } = body.key;
// CORREÇÃO LID: Resolve LID para número normal antes de processar
if (isLid && !isGroup) {
const resolvedPhone = await this.resolveLidToPhone(instance, body.key);
if (resolvedPhone && resolvedPhone !== remoteJid) {
this.logger.verbose(`LID detected and resolved: ${remoteJid}${resolvedPhone}`);
phoneNumber = resolvedPhone;
// Salva mapeamento se temos remoteJidAlt
if (body.key.remoteJidAlt) {
this.saveLidMapping(remoteJid, body.key.remoteJidAlt);
}
} else if (body.key.remoteJidAlt) {
// Se não resolveu mas tem remoteJidAlt, usa ele
phoneNumber = body.key.remoteJidAlt;
this.saveLidMapping(remoteJid, body.key.remoteJidAlt);
this.logger.verbose(`Using remoteJidAlt for LID: ${remoteJid}${phoneNumber}`);
}
}
// Usa phoneNumber como base para cache (não o LID)
const cacheKey = `${instance.instanceName}:createConversation-${phoneNumber}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${phoneNumber}`;
const maxWaitTime = 5000; // 5 seconds
const client = await this.clientCw(instance);
if (!client) return null;
@@ -943,20 +973,39 @@ export class ChatwootService {
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
// Filtra valores null/undefined do content_attributes para evitar erro 406
const filteredReplyToIds = Object.fromEntries(
Object.entries(replyToIds).filter(([_, value]) => value != null)
);
// Monta o objeto data, incluindo content_attributes apenas se houver dados válidos
const messageData: any = {
content: content,
message_type: messageType,
content_type: 'text', // Explicitamente define como texto para Chatwoot 4.x
attachments: attachments,
private: privateMessage || false,
};
// Adiciona source_id apenas se existir
if (sourceId) {
messageData.source_id = sourceId;
}
// Adiciona content_attributes apenas se houver dados válidos
if (Object.keys(filteredReplyToIds).length > 0) {
messageData.content_attributes = filteredReplyToIds;
}
// Adiciona source_reply_id apenas se existir
if (sourceReplyId) {
messageData.source_reply_id = sourceReplyId.toString();
}
const message = await client.messages.create({
accountId: this.provider.accountId,
conversationId: conversationId,
data: {
content: content,
message_type: messageType,
attachments: attachments,
private: privateMessage || false,
source_id: sourceId,
content_attributes: {
...replyToIds,
},
source_reply_id: sourceReplyId ? sourceReplyId.toString() : null,
},
data: messageData,
});
if (!message) {
@@ -1082,11 +1131,14 @@ export class ChatwootService {
if (messageBody && instance) {
const replyToIds = await this.getReplyToIds(messageBody, instance);
if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) {
const content = JSON.stringify({
...replyToIds,
});
data.append('content_attributes', content);
// Filtra valores null/undefined antes de enviar
const filteredReplyToIds = Object.fromEntries(
Object.entries(replyToIds).filter(([_, value]) => value != null)
);
if (Object.keys(filteredReplyToIds).length > 0) {
const contentAttrs = JSON.stringify(filteredReplyToIds);
data.append('content_attributes', contentAttrs);
}
}
@@ -1617,18 +1669,36 @@ export class ChatwootService {
return;
}
// Use raw SQL to avoid JSON path issues
const result = await this.prismaRepository.$executeRaw`
UPDATE "Message"
SET
"chatwootMessageId" = ${chatwootMessageIds.messageId},
"chatwootConversationId" = ${chatwootMessageIds.conversationId},
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
WHERE "instanceId" = ${instance.instanceId}
AND "key"->>'id' = ${key.id}
`;
const provider = this.configService.get<Database>('DATABASE').PROVIDER;
let result: number;
if (provider === 'mysql') {
// MySQL version
result = await this.prismaRepository.$executeRaw`
UPDATE Message
SET
chatwootMessageId = ${chatwootMessageIds.messageId},
chatwootConversationId = ${chatwootMessageIds.conversationId},
chatwootInboxId = ${chatwootMessageIds.inboxId},
chatwootContactInboxSourceId = ${chatwootMessageIds.contactInboxSourceId},
chatwootIsRead = ${chatwootMessageIds.isRead || false}
WHERE instanceId = ${instance.instanceId}
AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${key.id}
`;
} else {
// PostgreSQL version
result = await this.prismaRepository.$executeRaw`
UPDATE "Message"
SET
"chatwootMessageId" = ${chatwootMessageIds.messageId},
"chatwootConversationId" = ${chatwootMessageIds.conversationId},
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
WHERE "instanceId" = ${instance.instanceId}
AND "key"->>'id' = ${key.id}
`;
}
this.logger.verbose(`Update result: ${result} rows affected`);
@@ -1642,15 +1712,28 @@ export class ChatwootService {
}
private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise<MessageModel> {
// Use raw SQL query to avoid JSON path issues with Prisma
const messages = await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${instance.instanceId}
AND "key"->>'id' = ${keyId}
LIMIT 1
`;
const provider = this.configService.get<Database>('DATABASE').PROVIDER;
let messages: MessageModel[];
return (messages as MessageModel[])[0] || null;
if (provider === 'mysql') {
// MySQL version
messages = await this.prismaRepository.$queryRaw`
SELECT * FROM Message
WHERE instanceId = ${instance.instanceId}
AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${keyId}
LIMIT 1
`;
} else {
// PostgreSQL version
messages = await this.prismaRepository.$queryRaw`
SELECT * FROM "Message"
WHERE "instanceId" = ${instance.instanceId}
AND "key"->>'id' = ${keyId}
LIMIT 1
`;
}
return messages[0] || null;
}
private async getReplyToIds(
@@ -1758,41 +1841,127 @@ export class ChatwootService {
}
private getTypeMessage(msg: any) {
const types = {
conversation: msg.conversation,
imageMessage: msg.imageMessage?.caption,
videoMessage: msg.videoMessage?.caption,
extendedTextMessage: msg.extendedTextMessage?.text,
messageContextInfo: msg.messageContextInfo?.stanzaId,
stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
liveLocationMessage: msg.liveLocationMessage,
listMessage: msg.listMessage,
listResponseMessage: msg.listResponseMessage,
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
};
const types = {
conversation: msg.conversation,
imageMessage: msg.imageMessage?.caption,
videoMessage: msg.videoMessage?.caption,
extendedTextMessage: msg.extendedTextMessage?.text,
messageContextInfo: msg.messageContextInfo?.stanzaId,
stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
liveLocationMessage: msg.liveLocationMessage,
listMessage: msg.listMessage,
listResponseMessage: msg.listResponseMessage,
orderMessage: msg.orderMessage,
quotedProductMessage: msg.contextInfo?.quotedMessage?.productMessage,
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
};
return types;
}
return types;
}
private getMessageContent(types: any) {
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
let result = typeKey ? types[typeKey] : undefined;
// Remove externalAdReplyBody| in Chatwoot (Already Have)
// Remove externalAdReplyBody| in Chatwoot
if (result && typeof result === 'string' && result.includes('externalAdReplyBody|')) {
result = result.split('externalAdReplyBody|').filter(Boolean).join('');
}
// Tratamento de Pedidos do Catálogo (WhatsApp Business Catalog)
if (typeKey === 'orderMessage' && result.orderId) {
const now = Date.now();
// Limpa entradas antigas do cache
this.processedOrderIds.forEach((timestamp, id) => {
if (now - timestamp > this.ORDER_CACHE_TTL_MS) {
this.processedOrderIds.delete(id);
}
});
// Verifica se já processou este orderId
if (this.processedOrderIds.has(result.orderId)) {
return undefined; // Ignora duplicado
}
this.processedOrderIds.set(result.orderId, now);
}
// Tratamento de Produto citado (WhatsApp Desktop)
if (typeKey === 'quotedProductMessage' && result?.product) {
const product = result.product;
// Extrai preço
let rawPrice = 0;
const amount = product.priceAmount1000;
if (Long.isLong(amount)) {
rawPrice = amount.toNumber();
} else if (amount && typeof amount === 'object' && 'low' in amount) {
rawPrice = Long.fromValue(amount).toNumber();
} else if (typeof amount === 'number') {
rawPrice = amount;
}
const price = (rawPrice / 1000).toLocaleString('pt-BR', {
style: 'currency',
currency: product.currencyCode || 'BRL',
});
const productTitle = product.title || 'Produto do catálogo';
const productId = product.productId || 'N/A';
return (
`🛒 *PRODUTO DO CATÁLOGO (Desktop)*\n` +
`━━━━━━━━━━━━━━━━━━━━━\n` +
`📦 *Produto:* ${productTitle}\n` +
`💰 *Preço:* ${price}\n` +
`🆔 *Código:* ${productId}\n` +
`━━━━━━━━━━━━━━━━━━━━━\n` +
`_Cliente perguntou: "${types.conversation || 'Me envia este produto?'}"_`
);
}
if (typeKey === 'orderMessage') {
// Extrai o valor - pode ser Long, objeto {low, high}, ou número direto
let rawPrice = 0;
const amount = result.totalAmount1000;
if (Long.isLong(amount)) {
rawPrice = amount.toNumber();
} else if (amount && typeof amount === 'object' && 'low' in amount) {
// Formato {low: number, high: number, unsigned: boolean}
rawPrice = Long.fromValue(amount).toNumber();
} else if (typeof amount === 'number') {
rawPrice = amount;
}
const price = (rawPrice / 1000).toLocaleString('pt-BR', {
style: 'currency',
currency: result.totalCurrencyCode || 'BRL',
});
const itemCount = result.itemCount || 1;
const orderTitle = result.orderTitle || 'Produto do catálogo';
const orderId = result.orderId || 'N/A';
return (
`🛒 *NOVO PEDIDO NO CATÁLOGO*\n` +
`━━━━━━━━━━━━━━━━━━━━━\n` +
`📦 *Produto:* ${orderTitle}\n` +
`📊 *Quantidade:* ${itemCount}\n` +
`💰 *Total:* ${price}\n` +
`🆔 *Pedido:* #${orderId}\n` +
`━━━━━━━━━━━━━━━━━━━━━\n` +
`_Responda para atender este pedido!_`
);
}
if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') {
const latitude = result.degreesLatitude;
const longitude = result.degreesLongitude;
@@ -1993,6 +2162,29 @@ export class ChatwootService {
}
}
// CORREÇÃO LID: Resolve LID para número normal antes de processar evento
if (body?.key?.remoteJid && body.key.remoteJid.includes('@lid') && !body.key.remoteJid.endsWith('@g.us')) {
const originalJid = body.key.remoteJid;
const resolvedPhone = await this.resolveLidToPhone(instance, body.key);
if (resolvedPhone && resolvedPhone !== originalJid) {
this.logger.verbose(`Event LID resolved: ${originalJid}${resolvedPhone}`);
body.key.remoteJid = resolvedPhone;
// Salva mapeamento se temos remoteJidAlt
if (body.key.remoteJidAlt) {
this.saveLidMapping(originalJid, body.key.remoteJidAlt);
}
} else if (body.key.remoteJidAlt && !body.key.remoteJidAlt.includes('@lid')) {
// Se não resolveu mas tem remoteJidAlt válido, usa ele
this.logger.verbose(`Using remoteJidAlt for event: ${originalJid}${body.key.remoteJidAlt}`);
body.key.remoteJid = body.key.remoteJidAlt;
this.saveLidMapping(originalJid, body.key.remoteJidAlt);
} else {
this.logger.warn(`Could not resolve LID for event, keeping original: ${originalJid}`);
}
}
if (event === 'messages.upsert' || event === 'send.message') {
this.logger.info(`[${event}] New message received - Instance: ${JSON.stringify(body, null, 2)}`);
if (body.key.remoteJid === 'status@broadcast') {
@@ -2537,6 +2729,82 @@ export class ChatwootService {
return remoteJid.replace(/:\d+/, '').split('@')[0];
}
/**
* Limpa entradas antigas do cache de mapeamento LID
*/
private cleanLidCache() {
const now = Date.now();
this.lidToPhoneMap.forEach((value, lid) => {
if (now - value.timestamp > this.LID_CACHE_TTL_MS) {
this.lidToPhoneMap.delete(lid);
}
});
}
/**
* Salva mapeamento LID → Número Normal
*/
private saveLidMapping(lid: string, phoneNumber: string) {
if (!lid || !phoneNumber || !lid.includes('@lid')) {
return;
}
this.cleanLidCache();
this.lidToPhoneMap.set(lid, {
phone: phoneNumber,
timestamp: Date.now(),
});
this.logger.verbose(`LID mapping saved: ${lid}${phoneNumber}`);
}
/**
* Resolve LID para Número Normal
* Retorna o número normal se encontrado, ou o LID original se não encontrado
*/
private async resolveLidToPhone(instance: InstanceDto, messageKey: any): Promise<string | null> {
const { remoteJid, remoteJidAlt } = messageKey;
// Se não for LID, retorna o próprio remoteJid
if (!remoteJid || !remoteJid.includes('@lid')) {
return remoteJid;
}
// 1. Tenta buscar no cache
const cached = this.lidToPhoneMap.get(remoteJid);
if (cached) {
this.logger.verbose(`LID resolved from cache: ${remoteJid}${cached.phone}`);
return cached.phone;
}
// 2. Se tem remoteJidAlt (número alternativo), usa ele e salva no cache
if (remoteJidAlt && !remoteJidAlt.includes('@lid')) {
this.saveLidMapping(remoteJid, remoteJidAlt);
this.logger.verbose(`LID resolved from remoteJidAlt: ${remoteJid}${remoteJidAlt}`);
return remoteJidAlt;
}
// 3. Tenta buscar no banco de dados do Chatwoot
try {
const lidIdentifier = this.normalizeJidIdentifier(remoteJid);
const contact = await this.findContactByIdentifier(instance, lidIdentifier);
if (contact && contact.phone_number) {
// Converte +554498860240 → 554498860240@s.whatsapp.net
const phoneNumber = contact.phone_number.replace('+', '') + '@s.whatsapp.net';
this.saveLidMapping(remoteJid, phoneNumber);
this.logger.verbose(`LID resolved from database: ${remoteJid}${phoneNumber}`);
return phoneNumber;
}
} catch (error) {
this.logger.warn(`Error resolving LID from database: ${error}`);
}
// 4. Se não encontrou, retorna null (será necessário criar novo contato)
this.logger.warn(`Could not resolve LID: ${remoteJid}`);
return null;
}
public startImportHistoryMessages(instance: InstanceDto) {
if (!this.isImportHistoryAvailable()) {
return;
@@ -368,6 +368,60 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
sendTelemetry('/message/sendWhatsAppAudio');
}
if (message.type === 'file' || message.type === 'embed') {
const content = message.content as { url?: string; name?: string } | undefined;
if (!content?.url) {
sendTelemetry('/message/sendMediaMissingUrl');
return;
}
const mediaUrl = content.url;
const mediaType = this.getMediaType(mediaUrl);
let fileName = content.name;
if (!fileName) {
try {
const urlObj = new URL(mediaUrl);
const path = urlObj.pathname || '';
const candidate = path.split('/').pop() || '';
if (candidate && candidate.includes('.')) {
fileName = candidate;
}
} catch {
// Ignore URL parsing failures
}
if (!fileName) {
fileName = mediaType && mediaType !== 'document' ? `media.${mediaType}` : 'attachment';
}
}
if (mediaType === 'audio') {
await instance.audioWhatsapp(
{
number: session.remoteJid,
delay: settings?.delayMessage || 1000,
encoding: true,
audio: mediaUrl,
},
false,
);
} else {
await instance.mediaMessage(
{
number: session.remoteJid,
delay: settings?.delayMessage || 1000,
mediatype: mediaType || 'document',
media: mediaUrl,
fileName,
},
null,
false,
);
}
sendTelemetry('/message/sendMedia');
}
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
if (wait) {
@@ -162,6 +162,7 @@ export class EventController {
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
'MESSAGING_HISTORY_SET',
'REMOVE_INSTANCE',
'LOGOUT_INSTANCE',
'INSTANCE_CREATE',
@@ -126,7 +126,9 @@ export class SqsController extends EventController implements EventControllerInt
? 'singlequeue'
: `${event.replace('.', '_').toLowerCase()}`;
const queueName = `${prefixName}_${eventFormatted}.fifo`;
const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`;
const rawBaseUrl = sqsConfig.BASE_URL || `https://sqs.${sqsConfig.REGION}.amazonaws.com`;
const baseUrl = rawBaseUrl.replace(/\/+$/, '');
const sqsUrl = `${baseUrl}/${sqsConfig.ACCOUNT_ID}/${queueName}`;
const message = {
...(extra ?? {}),
@@ -124,9 +124,20 @@ export class WebhookController extends EventController implements EventControlle
try {
if (instance?.enabled && regex.test(instance.url)) {
// Add custom headers for better webhook tracking and debugging
const enhancedHeaders = {
...webhookHeaders,
'Content-Type': 'application/json',
'X-Instance-ID': this.monitor.waInstances[instanceName].instanceId,
'X-Instance-Name': instanceName,
'X-Event-Type': event,
'X-Timestamp': Date.now().toString(),
'User-Agent': 'EvolutionAPI-Webhook/2.3.7',
};
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
headers: enhancedHeaders as Record<string, string>,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
+22
View File
@@ -2,6 +2,7 @@ import { RouterBroker } from '@api/abstract/abstract.router';
import {
ArchiveChatDto,
BlockUserDto,
DecryptPollVoteDto,
DeleteMessage,
getBase64FromMediaMessageDto,
MarkChatUnreadDto,
@@ -23,6 +24,7 @@ import {
archiveChatSchema,
blockUserSchema,
contactValidateSchema,
decryptPollVoteSchema,
deleteMessageSchema,
markChatUnreadSchema,
messageUpSchema,
@@ -281,6 +283,26 @@ export class ChatRouter extends RouterBroker {
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('getPollVote'), ...guards, async (req, res) => {
const response = await this.dataValidate<DecryptPollVoteDto>({
request: req,
schema: decryptPollVoteSchema,
ClassRef: DecryptPollVoteDto,
execute: (instance, data) => chatController.decryptPollVote(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('findChannels'), ...guards, async (req, res) => {
const response = await this.dataValidate({
request: req,
schema: contactValidateSchema,
ClassRef: Query<Contact>,
execute: (instance, query) => chatController.fetchChannels(instance, query as any),
});
return res.status(HttpStatus.OK).json(response);
});
}
+118 -54
View File
@@ -9,7 +9,7 @@ import { TypebotService } from '@api/integrations/chatbot/typebot/services/typeb
import { PrismaRepository, Query } from '@api/repository/repository.service';
import { eventManager, waMonitor } from '@api/server.module';
import { Events, wa } from '@api/types/wa.types';
import { Auth, Chatwoot, ConfigService, HttpServer, Proxy } from '@config/env.config';
import { Auth, Chatwoot, ConfigService, Database, HttpServer, Proxy } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import { Contact, Message, Prisma } from '@prisma/client';
@@ -731,63 +731,127 @@ export class ChannelStartupService {
where['remoteJid'] = remoteJid;
}
const timestampFilter =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
const provider = this.configService.get<Database>('DATABASE').PROVIDER;
const limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``;
const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : Prisma.sql``;
const results = await this.prismaRepository.$queryRaw`
WITH rankedMessages AS (
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
"Contact"."id" as "contactId",
"Message"."key"->>'remoteJid' as "remoteJid",
CASE
WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName")
ELSE COALESCE("Contact"."pushName", "Message"."pushName")
END as "pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."name" as "pushName",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
"Chat"."unreadMessages" as "unreadMessages",
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
"Message"."id" AS "lastMessageId",
"Message"."key" AS "lastMessage_key",
let results: any[];
if (provider === 'mysql') {
// MySQL version
const timestampFilterMysql =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND Message.messageTimestamp >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND Message.messageTimestamp <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
results = await this.prismaRepository.$queryRaw`
SELECT
Contact.id as contactId,
JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) as remoteJid,
CASE
WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você'
ELSE "Message"."pushName"
END AS "lastMessagePushName",
"Message"."participant" AS "lastMessageParticipant",
"Message"."messageType" AS "lastMessageMessageType",
"Message"."message" AS "lastMessageMessage",
"Message"."contextInfo" AS "lastMessageContextInfo",
"Message"."source" AS "lastMessageSource",
"Message"."messageTimestamp" AS "lastMessageMessageTimestamp",
"Message"."instanceId" AS "lastMessageInstanceId",
"Message"."sessionId" AS "lastMessageSessionId",
"Message"."status" AS "lastMessageStatus"
FROM "Message"
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
WHERE "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST
${limit}
${offset};
`;
WHEN JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) LIKE '%@g.us' THEN COALESCE(Chat.name, Contact.pushName)
ELSE COALESCE(Contact.pushName, Message.pushName)
END as pushName,
Contact.profilePicUrl,
COALESCE(
FROM_UNIXTIME(Message.messageTimestamp),
Contact.updatedAt
) as updatedAt,
Chat.name as chatName,
Chat.createdAt as windowStart,
DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires,
Chat.unreadMessages as unreadMessages,
CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive,
Message.id AS lastMessageId,
Message.key AS lastMessage_key,
CASE
WHEN JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.fromMe')) = 'true' THEN 'Você'
ELSE Message.pushName
END AS lastMessagePushName,
Message.participant AS lastMessageParticipant,
Message.messageType AS lastMessageMessageType,
Message.message AS lastMessageMessage,
Message.contextInfo AS lastMessageContextInfo,
Message.source AS lastMessageSource,
Message.messageTimestamp AS lastMessageMessageTimestamp,
Message.instanceId AS lastMessageInstanceId,
Message.sessionId AS lastMessageSessionId,
Message.status AS lastMessageStatus
FROM Message
LEFT JOIN Contact ON Contact.remoteJid = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) AND Contact.instanceId = Message.instanceId
LEFT JOIN Chat ON Chat.remoteJid = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) AND Chat.instanceId = Message.instanceId
WHERE Message.instanceId = ${this.instanceId}
${remoteJid ? Prisma.sql`AND JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid')) = ${remoteJid}` : Prisma.sql``}
${timestampFilterMysql}
AND Message.messageTimestamp = (
SELECT MAX(m2.messageTimestamp)
FROM Message m2
WHERE JSON_UNQUOTE(JSON_EXTRACT(m2.key, '$.remoteJid')) = JSON_UNQUOTE(JSON_EXTRACT(Message.key, '$.remoteJid'))
AND m2.instanceId = Message.instanceId
)
ORDER BY updatedAt DESC
${limit}
${offset};
`;
} else {
// PostgreSQL version
const timestampFilter =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
results = await this.prismaRepository.$queryRaw`
WITH rankedMessages AS (
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
"Contact"."id" as "contactId",
"Message"."key"->>'remoteJid' as "remoteJid",
CASE
WHEN "Message"."key"->>'remoteJid' LIKE '%@g.us' THEN COALESCE("Chat"."name", "Contact"."pushName")
ELSE COALESCE("Contact"."pushName", "Message"."pushName")
END as "pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."name" as "pushName",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
"Chat"."unreadMessages" as "unreadMessages",
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
"Message"."id" AS "lastMessageId",
"Message"."key" AS "lastMessage_key",
CASE
WHEN "Message"."key"->>'fromMe' = 'true' THEN 'Você'
ELSE "Message"."pushName"
END AS "lastMessagePushName",
"Message"."participant" AS "lastMessageParticipant",
"Message"."messageType" AS "lastMessageMessageType",
"Message"."message" AS "lastMessageMessage",
"Message"."contextInfo" AS "lastMessageContextInfo",
"Message"."source" AS "lastMessageSource",
"Message"."messageTimestamp" AS "lastMessageMessageTimestamp",
"Message"."instanceId" AS "lastMessageInstanceId",
"Message"."sessionId" AS "lastMessageSessionId",
"Message"."status" AS "lastMessageStatus"
FROM "Message"
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
WHERE "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST
${limit}
${offset};
`;
}
if (results && isArray(results) && results.length > 0) {
const mappedResults = results.map((contact) => {
+17
View File
@@ -91,6 +91,7 @@ export type EventsRabbitmq = {
CALL: boolean;
TYPEBOT_START: boolean;
TYPEBOT_CHANGE_STATUS: boolean;
MESSAGING_HISTORY_SET: boolean;
};
export type Rabbitmq = {
@@ -121,6 +122,7 @@ export type Sqs = {
SECRET_ACCESS_KEY: string;
ACCOUNT_ID: string;
REGION: string;
BASE_URL: string;
MAX_PAYLOAD_SIZE: number;
EVENTS: {
APPLICATION_STARTUP: boolean;
@@ -150,6 +152,7 @@ export type Sqs = {
SEND_MESSAGE: boolean;
TYPEBOT_CHANGE_STATUS: boolean;
TYPEBOT_START: boolean;
MESSAGING_HISTORY_SET: boolean;
};
};
@@ -223,6 +226,7 @@ export type EventsWebhook = {
CALL: boolean;
TYPEBOT_START: boolean;
TYPEBOT_CHANGE_STATUS: boolean;
MESSAGING_HISTORY_SET: boolean;
ERRORS: boolean;
ERRORS_WEBHOOK: string;
};
@@ -256,6 +260,7 @@ export type EventsPusher = {
CALL: boolean;
TYPEBOT_START: boolean;
TYPEBOT_CHANGE_STATUS: boolean;
MESSAGING_HISTORY_SET: boolean;
};
export type ApiKey = { KEY: string };
@@ -313,6 +318,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 +416,7 @@ export interface Env {
WEBHOOK: Webhook;
PUSHER: Pusher;
CONFIG_SESSION_PHONE: ConfigSessionPhone;
BAILEYS: Baileys;
QRCODE: QrCode;
TYPEBOT: Typebot;
CHATWOOT: Chatwoot;
@@ -537,6 +544,7 @@ export class ConfigService {
CALL: process.env?.RABBITMQ_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.RABBITMQ_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
MESSAGING_HISTORY_SET: process.env?.RABBITMQ_EVENTS_MESSAGING_HISTORY_SET === 'true',
},
},
NATS: {
@@ -574,6 +582,7 @@ export class ConfigService {
CALL: process.env?.NATS_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
MESSAGING_HISTORY_SET: process.env?.NATS_EVENTS_MESSAGING_HISTORY_SET === 'true',
},
},
SQS: {
@@ -585,6 +594,7 @@ export class ConfigService {
SECRET_ACCESS_KEY: process.env.SQS_SECRET_ACCESS_KEY || '',
ACCOUNT_ID: process.env.SQS_ACCOUNT_ID || '',
REGION: process.env.SQS_REGION || '',
BASE_URL: process.env.SQS_BASE_URL || '',
MAX_PAYLOAD_SIZE: Number.parseInt(process.env.SQS_MAX_PAYLOAD_SIZE ?? '1048576'),
EVENTS: {
APPLICATION_STARTUP: process.env?.SQS_GLOBAL_APPLICATION_STARTUP === 'true',
@@ -614,6 +624,7 @@ export class ConfigService {
SEND_MESSAGE: process.env?.SQS_GLOBAL_SEND_MESSAGE === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.SQS_GLOBAL_TYPEBOT_CHANGE_STATUS === 'true',
TYPEBOT_START: process.env?.SQS_GLOBAL_TYPEBOT_START === 'true',
MESSAGING_HISTORY_SET: process.env?.SQS_GLOBAL_MESSAGING_HISTORY_SET === 'true',
},
},
KAFKA: {
@@ -657,6 +668,7 @@ export class ConfigService {
CALL: process.env?.KAFKA_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.KAFKA_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.KAFKA_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
MESSAGING_HISTORY_SET: process.env?.KAFKA_EVENTS_MESSAGING_HISTORY_SET === 'true',
},
SASL:
process.env?.KAFKA_SASL_ENABLED === 'true'
@@ -722,6 +734,7 @@ export class ConfigService {
CALL: process.env?.PUSHER_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.PUSHER_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.PUSHER_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
MESSAGING_HISTORY_SET: process.env?.PUSHER_EVENTS_MESSAGING_HISTORY_SET === 'true',
},
},
WA_BUSINESS: {
@@ -779,6 +792,7 @@ export class ConfigService {
CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.WEBHOOK_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
MESSAGING_HISTORY_SET: process.env?.WEBHOOK_EVENTS_MESSAGING_HISTORY_SET === 'true',
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
},
@@ -800,6 +814,9 @@ export class ConfigService {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome',
},
BAILEYS: {
VERSION: process.env?.CONFIG_BAILEYS_VERSION,
},
QRCODE: {
LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30,
COLOR: process.env.QRCODE_COLOR || '#198754',
+6 -1
View File
@@ -35,7 +35,12 @@ function formatBRNumber(jid: string) {
export function createJid(number: string): string {
number = number.replace(/:\d+/, '');
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
if (
number.includes('@g.us') ||
number.includes('@s.whatsapp.net') ||
number.includes('@lid') ||
number.includes('@newsletter')
) {
return number;
}
+73 -4
View File
@@ -1,7 +1,51 @@
import axios, { AxiosRequestConfig } from 'axios';
import { fetchLatestBaileysVersion, WAVersion } from 'baileys';
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
import { CacheService } from '../api/services/cache.service';
import { CacheEngine } from '../cache/cacheengine';
import { Baileys, configService } from '../config/env.config';
// Cache keys
const CACHE_KEY_WHATSAPP_WEB_VERSION = 'whatsapp_web_version';
const CACHE_KEY_BAILEYS_FALLBACK_VERSION = 'baileys_fallback_version';
// Cache TTL (1 hour in seconds)
const CACHE_TTL_SECONDS = 3600;
const MODULE_NAME = 'whatsapp-version';
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>, cache?: CacheService) => {
// 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,
};
}
}
let versionCache = cache || null;
if (!versionCache) {
// Cache estático para versões do WhatsApp Web e fallback do Baileys (fallback se não for passado via parâmetro)
const cacheEngine = new CacheEngine(configService, MODULE_NAME);
const engine = cacheEngine.getEngine();
const defaultVersionCache = new CacheService(engine);
versionCache = defaultVersionCache;
}
// Check cache for WhatsApp Web version
const cachedWaVersion = await versionCache.get(CACHE_KEY_WHATSAPP_WEB_VERSION);
if (cachedWaVersion) {
return cachedWaVersion;
}
try {
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
...options,
@@ -12,26 +56,51 @@ export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) =
const match = data.match(regex);
if (!match?.[1]) {
return {
// Check cache for Baileys fallback version
const cachedFallback = await versionCache.get(CACHE_KEY_BAILEYS_FALLBACK_VERSION);
if (cachedFallback) {
return cachedFallback;
}
// Fetch and cache Baileys fallback version
const fallbackVersion = {
version: (await fetchLatestBaileysVersion()).version as WAVersion,
isLatest: false,
error: {
message: 'Could not find client revision in the fetched content',
},
};
await versionCache.set(CACHE_KEY_BAILEYS_FALLBACK_VERSION, fallbackVersion, CACHE_TTL_SECONDS);
return fallbackVersion;
}
const clientRevision = match[1];
return {
const result = {
version: [2, 3000, +clientRevision] as WAVersion,
isLatest: true,
};
// Cache the successful result
await versionCache.set(CACHE_KEY_WHATSAPP_WEB_VERSION, result, CACHE_TTL_SECONDS);
return result;
} catch (error) {
return {
// Check cache for Baileys fallback version
const cachedFallback = await versionCache.get(CACHE_KEY_BAILEYS_FALLBACK_VERSION);
if (cachedFallback) {
return cachedFallback;
}
// Fetch and cache Baileys fallback version
const fallbackVersion = {
version: (await fetchLatestBaileysVersion()).version as WAVersion,
isLatest: false,
error,
};
await versionCache.set(CACHE_KEY_BAILEYS_FALLBACK_VERSION, fallbackVersion, CACHE_TTL_SECONDS);
return fallbackVersion;
}
};
+16 -6
View File
@@ -16,7 +16,7 @@ const getTypeMessage = (msg: any) => {
conversation: msg?.message?.conversation,
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
contactMessage: msg?.message?.contactMessage?.displayName,
locationMessage: msg?.message?.locationMessage?.degreesLatitude.toString(),
locationMessage: msg?.message?.locationMessage?.degreesLatitude?.toString(),
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
@@ -49,9 +49,18 @@ const getTypeMessage = (msg: any) => {
: ''
}`
: undefined,
externalAdReplyBody: msg?.contextInfo?.externalAdReply?.body
? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}`
: undefined,
// --- FIX FACEBOOK ADS START ---
externalAdReplyBody: msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.body
? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.body}`
: msg?.message?.extendedTextMessage?.contextInfo?.externalAdReply?.title
? `externalAdReplyBody|${msg.message.extendedTextMessage.contextInfo.externalAdReply.title}`
: msg?.contextInfo?.externalAdReply?.body
? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}`
: msg?.contextInfo?.externalAdReply?.title
? `externalAdReplyBody|${msg.contextInfo.externalAdReply.title}`
: undefined,
// --- FIX FACEBOOK ADS END ---
};
const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown';
@@ -60,7 +69,9 @@ const getTypeMessage = (msg: any) => {
};
const getMessageContent = (types: any) => {
const typeKey = Object.keys(types).find((key) => key !== 'externalAdReplyBody' && types[key] !== undefined);
const typeKey = Object.keys(types).find(
(key) => key !== 'externalAdReplyBody' && key !== 'messageType' && types[key] !== undefined,
);
let result = typeKey ? types[typeKey] : undefined;
@@ -73,7 +84,6 @@ const getMessageContent = (types: any) => {
export const getConversationMessage = (msg: any) => {
const types = getTypeMessage(msg);
const messageContent = getMessageContent(types);
return messageContent ?? '';
+23 -3
View File
@@ -1,6 +1,7 @@
import { prismaRepository } from '@api/server.module';
import { configService, Database } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Prisma } from '@prisma/client';
import dayjs from 'dayjs';
const logger = new Logger('OnWhatsappCache');
@@ -164,9 +165,28 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
logger.verbose(
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
);
await prismaRepository.isOnWhatsapp.create({
data: dataPayload,
});
try {
await prismaRepository.isOnWhatsapp.create({
data: dataPayload,
});
} catch (error: any) {
// Check for unique constraint violation (Prisma error code P2002)
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002' &&
(error.meta?.target as string[])?.includes('remoteJid')
) {
logger.verbose(
`[saveOnWhatsappCache] Race condition detected for ${remoteJid}, updating existing record instead.`,
);
await prismaRepository.isOnWhatsapp.update({
where: { remoteJid: remoteJid },
data: dataPayload,
});
} else {
throw error;
}
}
}
} catch (e) {
// Loga o erro mas não para a execução dos outros promises
+4
View File
@@ -86,6 +86,7 @@ export const instanceSchema: JSONSchema7 = {
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
'MESSAGING_HISTORY_SET',
],
},
},
@@ -123,6 +124,7 @@ export const instanceSchema: JSONSchema7 = {
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
'MESSAGING_HISTORY_SET',
],
},
},
@@ -160,6 +162,7 @@ export const instanceSchema: JSONSchema7 = {
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
'MESSAGING_HISTORY_SET',
],
},
},
@@ -197,6 +200,7 @@ export const instanceSchema: JSONSchema7 = {
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
'MESSAGING_HISTORY_SET',
],
},
},
+22
View File
@@ -447,3 +447,25 @@ export const buttonsMessageSchema: JSONSchema7 = {
},
required: ['number'],
};
export const decryptPollVoteSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
message: {
type: 'object',
properties: {
key: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
required: ['key'],
},
remoteJid: { type: 'string' },
},
required: ['message', 'remoteJid'],
};