diff --git a/.env.example b/.env.example index 73a3b40d..fd01fca4 100644 --- a/.env.example +++ b/.env.example @@ -401,3 +401,15 @@ LANGUAGE=en # PROXY_PROTOCOL=http # PROXY_USERNAME= # PROXY_PASSWORD= + + +# ═══════════════════════════════════════════════════════════════ +# PAPI LICENSE SYSTEM (for interactive messages: buttons, lists, carousel) +# ═══════════════════════════════════════════════════════════════ +# License key provided by PAPI admin +# Get your license key at: https://padmin.intrategica.com.br/register.html +# Interactive messages (buttons, lists, carousel) require a valid license +PAPI_LICENSE_KEY=2FE61805-4639DE08-EE554154-6F42B6FA + +# Admin server URL for license verification +PAPI_LICENSE_ADMIN_URL=https://padmin.intrategica.com.br/ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..51348984 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Force submodule updates on pull +* submodule=diff + diff --git a/.github/workflows/check_code_quality.yml b/.github/workflows/check_code_quality.yml index df156f21..048e0c2d 100644 --- a/.github/workflows/check_code_quality.yml +++ b/.github/workflows/check_code_quality.yml @@ -15,6 +15,15 @@ jobs: - uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify submodules + run: | + if [ ! -d "papi" ]; then + echo "Error: papi submodule not found" + exit 1 + fi + echo "Submodule papi verified" - name: Install Node uses: actions/setup-node@v5 diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index d15628d1..605ddb46 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -17,6 +17,15 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify submodules + run: | + if [ ! -d "papi" ]; then + echo "Error: papi submodule not found" + exit 1 + fi + echo "Submodule papi verified" - name: Docker meta id: meta diff --git a/.github/workflows/publish_docker_image_homolog.yml b/.github/workflows/publish_docker_image_homolog.yml index 05c1a3b6..9ffdcef1 100644 --- a/.github/workflows/publish_docker_image_homolog.yml +++ b/.github/workflows/publish_docker_image_homolog.yml @@ -17,6 +17,15 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify submodules + run: | + if [ ! -d "papi" ]; then + echo "Error: papi submodule not found" + exit 1 + fi + echo "Submodule papi verified" - name: Docker meta id: meta diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml index e7cd35a9..db1042d7 100644 --- a/.github/workflows/publish_docker_image_latest.yml +++ b/.github/workflows/publish_docker_image_latest.yml @@ -17,6 +17,15 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify submodules + run: | + if [ ! -d "papi" ]; then + echo "Error: papi submodule not found" + exit 1 + fi + echo "Submodule papi verified" - name: Docker meta id: meta diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b83f0b5d..a9ea10cd 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -28,6 +28,15 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify submodules + run: | + if [ ! -d "papi" ]; then + echo "Error: papi submodule not found" + exit 1 + fi + echo "Submodule papi verified" - name: Initialize CodeQL uses: github/codeql-action/init@v3 @@ -51,5 +60,6 @@ jobs: uses: actions/checkout@v5 with: submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} - name: Dependency Review uses: actions/dependency-review-action@v4 diff --git a/.gitignore b/.gitignore index 768d8afa..7e270382 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ lerna-debug.log* .tool-versions /prisma/migrations/* + +# PAPI License machine ID (should not be committed) +.machine-id diff --git a/.gitmodules b/.gitmodules index ef5b3e63..e13dece3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "evolution-manager-v2"] path = evolution-manager-v2 url = https://github.com/EvolutionAPI/evolution-manager-v2.git +[submodule "papi"] + path = papi + url = https://github.com/DavidsonGomes/papi.git + branch = main diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index a72538ac..00000000 --- a/.husky/pre-push +++ /dev/null @@ -1,2 +0,0 @@ -npm run build -npm run lint:check diff --git a/Dockerfile b/Dockerfile index 24c4e3bc..e65a9439 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,11 +21,20 @@ COPY ./prisma ./prisma COPY ./manager ./manager COPY ./.env.example ./.env COPY ./runWithProvider.js ./ +COPY ./.gitmodules ./.gitmodules + +# Copiar submódulo papi (necessário para o build) +COPY ./papi ./papi COPY ./Docker ./Docker RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* +# Se o diretório papi não existir, tentar inicializar submódulos (caso build via git clone) +RUN if [ ! -d "./papi" ] && [ -f ".gitmodules" ]; then \ + git submodule update --init --recursive || echo "Submodules already initialized or not available"; \ + fi + RUN ./Docker/scripts/generate_database.sh RUN npm run build diff --git a/README.md b/README.md index e07c6241..24688942 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,34 @@ Evolution API supports multiple types of connections to WhatsApp, enabling flexi - The Cloud API supports features such as end-to-end encryption, advanced analytics, and more comprehensive customer service tools. - To use this API, you must comply with Meta's policies and potentially pay for usage based on message volume and other factors. +## Interactive Messages + +Evolution API uses [PAPI (Pastorini API)](https://github.com/mktpastorini/papi) as a Git submodule to provide robust support for interactive WhatsApp messages. PAPI is integrated directly into the codebase and powers the following features: + +- **Interactive Buttons**: Send reply buttons, URL buttons, call buttons, and copy code buttons +- **List Messages**: Create interactive menus with sections and options +- **Carousel Messages**: Send carousel cards with images, videos, and buttons + +These interactive message types are fully integrated and work seamlessly with Evolution API's instance management system. The PAPI library is included as a required submodule and is automatically updated when you pull the repository with `--recursive` or update submodules. + +**Note**: When cloning the repository, make sure to use `git clone --recursive` or run `git submodule update --init --recursive` after cloning to ensure the PAPI submodule is properly initialized. + +### License Requirement + +Interactive messages (buttons, lists, and carousel) require a valid PAPI license to function. To obtain a license key: + +1. **Register for a license**: Visit [https://padmin.intrategica.com.br/register.html](https://padmin.intrategica.com.br/register.html) to create an account and request a license key +2. **Support the project**: Consider contributing to the [PAPI crowdfunding campaign](https://papi.mundoautomatik.com/) to help fund the development of new features + +Once you have your license key, configure it in your `.env` file: + +```env +PAPI_LICENSE_KEY=your_license_key_here +PAPI_LICENSE_ADMIN_URL=https://padmin.intrategica.com.br/ +``` + +If no license is configured, interactive messages will be disabled. The license system verifies your license status periodically and blocks interactive messages if the license is invalid, expired, or blocked. + ## Integrations Evolution API supports various integrations to enhance its functionality. Below is a list of available integrations and their uses: diff --git a/evolution-manager-v2 b/evolution-manager-v2 new file mode 160000 index 00000000..77ee1a51 --- /dev/null +++ b/evolution-manager-v2 @@ -0,0 +1 @@ +Subproject commit 77ee1a51666db0003f8737987fd929b961d006e6 diff --git a/package-lock.json b/package-lock.json index 0447dc00..f3ccf277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -810,52 +809,6 @@ "node": ">=6.9.0" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.0.tgz", - "integrity": "sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.6.tgz", - "integrity": "sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==", - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.3.2", - "@keyv/bigmap": "^1.3.0", - "hookified": "^1.13.0", - "keyv": "^5.5.4" - } - }, - "node_modules/@cacheable/node-cache": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", - "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", - "license": "MIT", - "dependencies": { - "cacheable": "^2.3.1", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.2.tgz", - "integrity": "sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==", - "license": "MIT", - "dependencies": { - "hashery": "^1.2.0", - "keyv": "^5.5.4" - } - }, "node_modules/@commitlint/cli": { "version": "19.8.1", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", @@ -2655,28 +2608,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@keyv/bigmap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.0.tgz", - "integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==", - "license": "MIT", - "dependencies": { - "hashery": "^1.2.0", - "hookified": "^1.13.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.5.4" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "license": "MIT" - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3338,70 +3269,6 @@ "@opentelemetry/api": "^1.8" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -4507,51 +4374,6 @@ "node": ">=18" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/token-types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", - "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.1.0", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -4661,12 +4483,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5442,15 +5258,6 @@ "node": ">= 0.4" } }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5542,60 +5349,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/baileys": { - "version": "7.0.0-rc.9", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.9.tgz", - "integrity": "sha512-Txd2dZ9MHbojvsHckeuCnAKPO/bQjKxua/0tQSJwOKXffK5vpS82k4eA/Nb46K0cK0Bx+fyY0zhnQHYMBriQcw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cacheable/node-cache": "^1.4.0", - "@hapi/boom": "^9.1.3", - "async-mutex": "^0.5.0", - "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", - "lru-cache": "^11.1.0", - "music-metadata": "^11.7.0", - "p-queue": "^9.0.0", - "pino": "^9.6", - "protobufjs": "^7.2.4", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "audio-decode": "^2.1.3", - "jimp": "^1.6.0", - "link-preview-js": "^3.0.0", - "sharp": "*" - }, - "peerDependenciesMeta": { - "audio-decode": { - "optional": true - }, - "jimp": { - "optional": true - }, - "link-preview-js": { - "optional": true - } - } - }, - "node_modules/baileys/node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/baileys/node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5861,19 +5614,6 @@ "node": ">=8" } }, - "node_modules/cacheable": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.1.tgz", - "integrity": "sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==", - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.0.6", - "@cacheable/utils": "^2.3.2", - "hookified": "^1.14.0", - "keyv": "^5.5.5", - "qified": "^0.5.3" - } - }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -6557,12 +6297,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/curve25519-js": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", - "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", - "license": "MIT" - }, "node_modules/cz-conventional-changelog": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", @@ -9053,18 +8787,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hashery": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.3.0.tgz", - "integrity": "sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==", - "license": "MIT", - "dependencies": { - "hookified": "^1.13.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -9090,12 +8812,6 @@ "node": ">=0.10.0" } }, - "node_modules/hookified": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.14.0.tgz", - "integrity": "sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==", - "license": "MIT" - }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -10222,15 +9938,6 @@ "node": ">=14.0.0" } }, - "node_modules/keyv": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", - "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10251,54 +9958,6 @@ "integrity": "sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==", "license": "MIT" }, - "node_modules/libsignal": { - "name": "@whiskeysockets/libsignal-node", - "version": "2.0.1", - "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67", - "license": "GPL-3.0", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "6.8.8" - } - }, - "node_modules/libsignal/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "license": "MIT" - }, - "node_modules/libsignal/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "license": "Apache-2.0" - }, - "node_modules/libsignal/node_modules/protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10846,15 +10505,6 @@ "node": ">=0.10.0" } }, - "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -10873,15 +10523,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mediainfo.js": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.3.6.tgz", @@ -11278,98 +10919,6 @@ "node": ">= 10.16.0" } }, - "node_modules/music-metadata": { - "version": "11.10.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.10.3.tgz", - "integrity": "sha512-j0g/x4cNNZW6I5gdcPAY+GFkJY9WHTpkFDMBJKQLxJQyvSfQbXm57fTE3haGFFuOzCgtsTd4Plwc49Sn9RacDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.0", - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "file-type": "^21.1.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.5.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/music-metadata/node_modules/file-type": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.1.tgz", - "integrity": "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/music-metadata/node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", - "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.1.0", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types/node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -11899,34 +11448,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-queue": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.0.1.tgz", - "integrity": "sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -12462,30 +11983,6 @@ ], "license": "MIT" }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12553,18 +12050,6 @@ "node": ">= 4.0.0" } }, - "node_modules/qified": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.3.tgz", - "integrity": "sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==", - "license": "MIT", - "dependencies": { - "hookified": "^1.13.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/qoa-format": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/qoa-format/-/qoa-format-1.0.1.tgz", @@ -14679,18 +14164,6 @@ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -15084,27 +14557,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", diff --git a/package.json b/package.json index 96e73179..3441935c 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", diff --git a/papi b/papi new file mode 160000 index 00000000..b04a3ee1 --- /dev/null +++ b/papi @@ -0,0 +1 @@ +Subproject commit b04a3ee1d8e1ce40c929fbd79d6c2abeb3709f5a diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index 64aa1c84..48615425 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -2,6 +2,7 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { SendAudioDto, SendButtonsDto, + SendCarouselDto, SendContactDto, SendListDto, SendLocationDto, @@ -14,8 +15,9 @@ import { SendTemplateDto, SendTextDto, } from '@api/dto/sendMessage.dto'; +import { licenseService } from '@api/services/license.service'; import { WAMonitoringService } from '@api/services/monitor.service'; -import { BadRequestException } from '@exceptions'; +import { BadRequestException, ForbiddenException } from '@exceptions'; import { isBase64, isURL } from 'class-validator'; import emojiRegex from 'emoji-regex'; @@ -75,14 +77,37 @@ export class SendMessageController { } public async sendButtons({ instanceName }: InstanceDto, data: SendButtonsDto) { + if (!(await licenseService.isAllowed())) { + const status = await licenseService.getStatus(); + throw new ForbiddenException( + `Interactive messages (buttons) are blocked. License status: ${status.status}. ${status.message}`, + ); + } return await this.waMonitor.waInstances[instanceName].buttonMessage(data); } + public async sendCarousel({ instanceName }: InstanceDto, data: SendCarouselDto) { + if (!(await licenseService.isAllowed())) { + const status = await licenseService.getStatus(); + throw new ForbiddenException( + `Interactive messages (carousel) are blocked. License status: ${status.status}. ${status.message}`, + ); + } + return await this.waMonitor.waInstances[instanceName].carouselMessage(data); + } + public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { return await this.waMonitor.waInstances[instanceName].locationMessage(data); } public async sendList({ instanceName }: InstanceDto, data: SendListDto) { + const instancesCount = Object.keys(this.waMonitor.waInstances).length; + if (!(await licenseService.isAllowed(instancesCount))) { + const status = await licenseService.getStatus(); + throw new ForbiddenException( + `Interactive messages (list) are blocked. License status: ${status.status}. ${status.message}`, + ); + } return await this.waMonitor.waInstances[instanceName].listMessage(data); } diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index ba9ecf52..1076647b 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -167,3 +167,34 @@ export class SendReactionDto { key: proto.IMessageKey; reaction: string; } + +export class CarouselCard { + title?: string; + footer?: string; + body?: string; + imageUrl?: string; + header?: { + title?: string; + subtitle?: string; + imageUrl?: string; + videoUrl?: string; + }; + buttons?: Array<{ + id?: string; + title?: string; + displayText?: string; + url?: string; + phoneNumber?: string; + quickReplyButton?: { id: string }; + urlButton?: { url: string }; + callButton?: { phoneNumber: string }; + copyCodeButton?: { code: string }; + }>; +} + +export class SendCarouselDto extends Metadata { + title?: string; + body?: string; + footer?: string; + cards: CarouselCard[]; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 3ef896c6..41b777cd 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -39,6 +39,7 @@ import { Options, SendAudioDto, SendButtonsDto, + SendCarouselDto, SendContactDto, SendListDto, SendLocationDto, @@ -65,7 +66,6 @@ import { CacheConf, ConfigService, configService, - ConfigSessionPhone, Database, Log, ProviderSession, @@ -86,8 +86,10 @@ import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files'; import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db'; import axios from 'axios'; +import type { Label, LabelAssociation } from 'baileys'; import makeWASocket, { AnyMessageContent, + Browsers, BufferedEventData, BufferJSON, CacheStore, @@ -100,6 +102,9 @@ import makeWASocket, { DisconnectReason, downloadContentFromMessage, downloadMediaMessage, + generateButtonMessage, + generateCarouselMessage, + generateListMessage, generateWAMessageFromContent, getAggregateVotesInPollMessage, GetCatalogOptions, @@ -120,15 +125,12 @@ import makeWASocket, { Product, proto, UserFacingSocketConfig, - WABrowserDescription, WAMediaUpload, WAMessage, WAMessageKey, WAPresence, WASocket, } from 'baileys'; -import { Label } from 'baileys/lib/Types/Label'; -import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; import { isArray, isBase64, isURL } from 'class-validator'; import { createHash } from 'crypto'; import EventEmitter2 from 'eventemitter2'; @@ -136,7 +138,6 @@ import FormData from 'form-data'; import Long from 'long'; import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; -import { release } from 'os'; import { join } from 'path'; import P from 'pino'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; @@ -581,7 +582,7 @@ export class BaileysStartupService extends ChannelStartupService { private async createClient(number?: string): Promise { this.instance.authState = await this.defineAuthState(); - const session = this.configService.get('CONFIG_SESSION_PHONE'); + // const session = this.configService.get('CONFIG_SESSION_PHONE'); let browserOptions = {}; @@ -590,10 +591,9 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.info(`Phone number: ${number}`); } else { - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - browserOptions = { browser }; - - this.logger.info(`Browser: ${browser}`); + // Usar exatamente como no papi/instanceManager.ts + browserOptions = { browser: Browsers.ubuntu('Chrome') }; + this.logger.info(`Browser: ${Browsers.ubuntu('Chrome')}`); } // Fetch latest WhatsApp Web version automatically @@ -3042,78 +3042,176 @@ export class BaileysStartupService extends ChannelStartupService { throw new BadRequestException('PIX button cannot be mixed with other button types'); } - const message: proto.IMessage = { + // Resolver JID antes de gerar a mensagem (igual ao PAPI) + const resolvedJid = await this.resolveJid(data.number); + + // PIX usa nativeFlowMessage (tratamento especial) + const message = { viewOnceMessage: { message: { interactiveMessage: { nativeFlowMessage: { buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + messageParamsJson: JSON.stringify({ from: 'api', templateId: cuid() }), }, }, }, }, - }; + } as any; - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); + this.logger.verbose({ message: '[PIX] Sending via sendMessage...' }); + this.logger.verbose({ message: '[PIX] Content', content: JSON.stringify(message, null, 2) }); + + // Enviar diretamente com sendMessage (igual ao PAPI) + await this.client.sendMessage(resolvedJid, message); + + this.logger.verbose({ message: '[PIX] Message sent successfully to', jid: resolvedJid }); + return { success: true, jid: resolvedJid }; } - const generate = await (async () => { - if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); - } - })(); + try { + // Resolver JID antes de gerar a mensagem (igual ao PAPI) + const resolvedJid = await this.resolveJid(data.number); - const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; - }); + // Mapear para o formato do papi/server.ts: { jid, text, footer, buttons, headerType, mediaUrl } + // generateButtonMessage(buttons, text, footer, headerType, mediaUrl) + // Combinar title e description em text (se description existir, usa ele; senão usa title) + const text = data.description || data.title || ''; + const footer = data.footer || ''; + const headerType = data?.thumbnailUrl ? 'image' : undefined; + const mediaUrl = data?.thumbnailUrl || ''; - const message: proto.IMessage = { - viewOnceMessage: { - message: { - interactiveMessage: { - body: { - text: (() => { - let t = '*' + data.title + '*'; - if (data?.description) { - t += '\n\n'; - t += data.description; - t += '\n'; - } - return t; - })(), - }, - footer: { text: data?.footer }, - header: (() => { - if (generate?.message?.imageMessage) { - return { - hasMediaAttachment: !!generate.message.imageMessage, - imageMessage: generate.message.imageMessage, - }; - } - })(), - nativeFlowMessage: { - buttons: buttons, - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), - }, + // Usar generateButtonMessage exatamente como no papi/server.ts + const buttonsInteractive = generateButtonMessage(data.buttons, text, footer, headerType, mediaUrl); + + this.logger.verbose({ message: '[Buttons] Sending via sendMessage...' }); + this.logger.verbose({ message: '[Buttons] Content', content: JSON.stringify(buttonsInteractive, null, 2) }); + + // Enviar diretamente com sendMessage (igual ao PAPI) + await this.client.sendMessage(resolvedJid, { + viewOnceMessage: { + message: { + interactiveMessage: buttonsInteractive, }, }, - }, - }; + } as any); - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); + this.logger.verbose({ message: '[Buttons] Message sent successfully to', jid: resolvedJid }); + return { success: true, jid: resolvedJid }; + } catch (error) { + this.logger.error({ message: '[Buttons] Error sending button message', error }); + throw new InternalServerErrorException('Failed to send button message', error?.toString()); + } + } + + public async carouselMessage(data: SendCarouselDto) { + try { + // Resolver JID antes de gerar a mensagem (igual ao PAPI) + const resolvedJid = await this.resolveJid(data.number); + + this.logger.verbose({ message: '[Carousel] Generating message content...' }); + + // Mapear cards do formato da API para o formato do PAPI + const cards: any[] = + data.cards && data.cards.length > 0 + ? data.cards.map((card: any) => ({ + header: { + title: card.header?.title || card.title || 'Produto', + subtitle: card.header?.subtitle || card.footer || '', + imageUrl: card.header?.imageUrl || card.imageUrl, + videoUrl: card.header?.videoUrl, + }, + body: card.body || '', + footer: card.footer || '', + buttons: (card.buttons || []).map((btn: any) => { + // Converter formato simplificado para formato Baileys + if (btn.id && btn.title) { + return { displayText: btn.title, quickReplyButton: { id: btn.id } }; + } + if (btn.displayText && btn.quickReplyButton) { + return { displayText: btn.displayText, quickReplyButton: btn.quickReplyButton }; + } + if (btn.displayText && btn.urlButton) { + return { displayText: btn.displayText, urlButton: btn.urlButton }; + } + if (btn.displayText && btn.callButton) { + return { displayText: btn.displayText, callButton: btn.callButton }; + } + if (btn.displayText && btn.copyCodeButton) { + return { displayText: btn.displayText, copyCodeButton: btn.copyCodeButton }; + } + return btn; + }), + })) + : []; + + // Upload media for cards (igual ao PAPI) + for (const card of cards) { + if (card.header?.imageUrl) { + try { + this.logger.verbose({ message: `[Carousel] Uploading image for card: ${card.header.title}` }); + const mediaInput = isURL(card.header.imageUrl) + ? { url: card.header.imageUrl } + : Buffer.from(card.header.imageUrl, 'base64'); + + const message = await prepareWAMessageMedia({ image: mediaInput } as any, { + upload: this.client.waUploadToServer, + }); + card.header.imageMessage = message.imageMessage; + } catch (error) { + this.logger.warn({ message: `[Carousel] Failed to upload image for card ${card.header.title}`, error }); + } + } + if (card.header?.videoUrl) { + try { + this.logger.verbose({ message: `[Carousel] Uploading video for card: ${card.header.title}` }); + const mediaInput = isURL(card.header.videoUrl) + ? { url: card.header.videoUrl } + : Buffer.from(card.header.videoUrl, 'base64'); + + const message = await prepareWAMessageMedia({ video: mediaInput } as any, { + upload: this.client.waUploadToServer, + }); + card.header.videoMessage = message.videoMessage; + } catch (error) { + this.logger.warn({ message: `[Carousel] Failed to upload video for card ${card.header.title}`, error }); + } + } + } + + this.logger.verbose({ message: '[Carousel] Cards', cards: JSON.stringify(cards, null, 2) }); + this.logger.verbose({ + message: '[Carousel] Title, Body, Footer', + title: data.title, + body: data.body, + footer: data.footer, + }); + + // generateCarouselMessage aceita apenas { cards } (igual ao PAPI) + const carouselContent = generateCarouselMessage({ cards }); + + this.logger.verbose({ message: '[Carousel] Sending message via relayMessage...' }); + this.logger.verbose({ message: '[Carousel] JID Original', jid: data.number }); + this.logger.verbose({ message: '[Carousel] JID Resolvido', jid: resolvedJid }); + + // Enviar diretamente como interactiveMessage (sem viewOnceMessage wrapper) - igual ao PAPI + const messageContent: proto.IMessage = { + interactiveMessage: carouselContent, + }; + + this.logger.verbose({ + message: '[Carousel] Message Content (sem viewOnce)', + content: JSON.stringify(messageContent, null, 2), + }); + + await this.client.relayMessage(resolvedJid, messageContent, {}); + this.logger.verbose({ message: '[Carousel] Message sent successfully to', jid: resolvedJid }); + + return { success: true, jid: resolvedJid, jidOriginal: data.number }; + } catch (error) { + this.logger.error({ message: '[Carousel] Failed to send message', error }); + throw new InternalServerErrorException('Failed to send carousel message', error?.toString()); + } } public async locationMessage(data: SendLocationDto) { @@ -3137,27 +3235,145 @@ export class BaileysStartupService extends ChannelStartupService { ); } + /** + * Normaliza um número de telefone para o formato JID do WhatsApp + * Trata especialmente números brasileiros com o 9º dígito + */ + private normalizeJid(phone: string): string { + if (!phone) return phone; + + // Se já é um JID de grupo, retorna como está + if (phone.includes('@g.us') || phone.includes('@broadcast') || phone.includes('@lid')) { + return phone; + } + + // Remove o sufixo @s.whatsapp.net se existir + let number = phone.replace('@s.whatsapp.net', '').replace('@c.us', ''); + + // Remove caracteres não numéricos (exceto +) + number = number.replace(/[^\d+]/g, ''); + + // Remove o + do início se existir + number = number.replace(/^\+/, ''); + + // Tratamento especial para números brasileiros + if (number.startsWith('55')) { + const withoutCountry = number.substring(2); // Remove o 55 + + if (withoutCountry.length === 11) { + const ddd = withoutCountry.substring(0, 2); + const ninthDigit = withoutCountry.substring(2, 3); + const restOfNumber = withoutCountry.substring(3); + + if (ninthDigit === '9') { + const firstDigitAfterNine = restOfNumber.substring(0, 1); + const dddNum = parseInt(ddd); + + // Para DDDs fora de SP (20+), remove o 9 se presente + if (dddNum >= 20 && ['9', '8', '7'].includes(firstDigitAfterNine)) { + number = '55' + ddd + restOfNumber; + this.logger.verbose(`[JID] Número brasileiro normalizado (removido 9): ${phone} -> ${number}`); + } + } + } + } + + return `${number}@s.whatsapp.net`; + } + + /** + * Verifica se um número existe no WhatsApp e retorna o JID correto + * Útil para resolver problemas de 9º dígito e LID + */ + private async resolveJid(phone: string): Promise { + const normalizedJid = this.normalizeJid(phone); + this.logger.verbose({ message: `[JID] Iniciando resolução: ${phone} -> normalizado: ${normalizedJid}` }); + + try { + // Tenta verificar se o número existe + const numberOnly = normalizedJid.replace('@s.whatsapp.net', ''); + this.logger.verbose({ message: `[JID] Verificando número: ${numberOnly}` }); + + const results = await this.client.onWhatsApp(numberOnly); + this.logger.verbose({ message: '[JID] Resultado onWhatsApp', results }); + + const [result] = results || []; + + if (result?.exists && result?.jid) { + this.logger.verbose({ message: `[JID] ✓ Número verificado: ${phone} -> ${result.jid}` }); + if (result.jid.includes('@lid')) { + this.logger.verbose({ message: `[JID] ⚠ Número usa formato LID (novo formato Meta)` }); + } + return result.jid; + } + + this.logger.verbose({ message: `[JID] Número não encontrado diretamente, tentando variações...` }); + + // Se não encontrou, tenta com/sem o 9 + const number = numberOnly; + if (number.startsWith('55') && number.length === 12) { + // Tenta adicionar o 9 + const withNine = number.substring(0, 4) + '9' + number.substring(4); + this.logger.verbose({ message: `[JID] Tentando com 9: ${withNine}` }); + const [resultWithNine] = (await this.client.onWhatsApp(withNine)) || []; + if (resultWithNine?.exists && resultWithNine?.jid) { + this.logger.verbose({ message: `[JID] ✓ Número encontrado com 9: ${phone} -> ${resultWithNine.jid}` }); + return resultWithNine.jid; + } + } else if (number.startsWith('55') && number.length === 13) { + // Tenta remover o 9 + const withoutNine = number.substring(0, 4) + number.substring(5); + this.logger.verbose({ message: `[JID] Tentando sem 9: ${withoutNine}` }); + const [resultWithoutNine] = (await this.client.onWhatsApp(withoutNine)) || []; + if (resultWithoutNine?.exists && resultWithoutNine?.jid) { + this.logger.verbose({ message: `[JID] ✓ Número encontrado sem 9: ${phone} -> ${resultWithoutNine.jid}` }); + return resultWithoutNine.jid; + } + } + + this.logger.verbose({ message: `[JID] ✗ Número não encontrado em nenhuma variação` }); + } catch (error) { + this.logger.warn({ message: '[JID] Erro ao verificar número', error }); + } + + // Retorna o JID normalizado se não conseguiu verificar + this.logger.verbose({ message: `[JID] Usando JID normalizado (não verificado): ${normalizedJid}` }); + return normalizedJid; + } + public async listMessage(data: SendListDto) { - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.title, - description: data.description, - buttonText: data?.buttonText, - footerText: data?.footerText, - sections: data.sections, - listType: 2, + try { + // Resolver JID antes de gerar a mensagem (igual ao PAPI) + const resolvedJid = await this.resolveJid(data.number); + + // Mapear para o formato do papi/server.ts: { jid, title, text, footer, buttonText, sections } + // generateListMessage(sections, buttonText, text, title, footer) + const listInteractive = generateListMessage( + data.sections, + data.buttonText, + data.description || '', // text + data.title, // title + data.footerText || '', // footer + ); + + this.logger.verbose({ message: '[List] Sending via sendMessage...' }); + this.logger.verbose({ message: '[List] Content', content: JSON.stringify(listInteractive, null, 2) }); + + // Enviar diretamente com sendMessage (igual ao PAPI) + await this.client.sendMessage(resolvedJid, { + viewOnceMessage: { + message: { + interactiveMessage: listInteractive, + }, }, - }, - { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }, - ); + } as any); + + this.logger.verbose({ message: '[List] Message sent successfully to', jid: resolvedJid }); + return { success: true, jid: resolvedJid }; + } catch (error) { + this.logger.error({ message: '[List] Error sending list message', error }); + throw new InternalServerErrorException('Failed to send list message', error?.toString()); + } } public async contactMessage(data: SendContactDto) { diff --git a/src/api/routes/sendMessage.router.ts b/src/api/routes/sendMessage.router.ts index cd073dba..51876483 100644 --- a/src/api/routes/sendMessage.router.ts +++ b/src/api/routes/sendMessage.router.ts @@ -2,6 +2,7 @@ import { RouterBroker } from '@api/abstract/abstract.router'; import { SendAudioDto, SendButtonsDto, + SendCarouselDto, SendContactDto, SendListDto, SendLocationDto, @@ -18,6 +19,7 @@ import { sendMessageController } from '@api/server.module'; import { audioMessageSchema, buttonsMessageSchema, + carouselMessageSchema, contactMessageSchema, listMessageSchema, locationMessageSchema, @@ -180,6 +182,16 @@ export class MessageRouter extends RouterBroker { execute: (instance, data) => sendMessageController.sendButtons(instance, data), }); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendCarousel'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: carouselMessageSchema, + ClassRef: SendCarouselDto, + execute: (instance, data) => sendMessageController.sendCarousel(instance, data), + }); + return res.status(HttpStatus.CREATED).json(response); }); } diff --git a/src/api/services/license.service.ts b/src/api/services/license.service.ts new file mode 100644 index 00000000..a2caa670 --- /dev/null +++ b/src/api/services/license.service.ts @@ -0,0 +1,152 @@ +import { configService, PapiLicense } from '@config/env.config'; +import { Logger } from '@config/logger.config'; + +import licenseManager from '../../../papi/lib/License/licenseManager.js'; + +export class LicenseService { + private logger = new Logger('LICENSE'); + private initialized = false; + private initializing = false; + + /** + * Initialize license manager with environment variables (lazy initialization) + * Only initializes when interactive messages are actually used + */ + private async ensureInitialized(): Promise { + // Se já está inicializado, retorna + if (this.initialized) { + return; + } + + // Se já está inicializando, aguarda + if (this.initializing) { + // Aguarda até 5 segundos para a inicialização completar + let attempts = 0; + while (this.initializing && attempts < 50) { + await new Promise((resolve) => setTimeout(resolve, 100)); + attempts++; + } + return; + } + + this.initializing = true; + + try { + const licenseConfig = configService.get('PAPI_LICENSE'); + + // Se não há configuração de licença, bloqueia mensagens interativas + if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) { + this.logger.warn( + '⚠️ PAPI License not configured - Interactive messages (buttons, lists, carousel) will be BLOCKED', + ); + this.logger.warn( + 'To enable interactive messages, configure PAPI_LICENSE_KEY and PAPI_LICENSE_ADMIN_URL in your .env file', + ); + this.logger.warn('Get your license key at: https://padmin.intrategica.com.br/register.html'); + this.initializing = false; + return; + } + + this.logger.verbose('Initializing PAPI License Manager...'); + + await licenseManager.initialize(licenseConfig.KEY, licenseConfig.ADMIN_URL); + + // Set callback for when license is blocked + licenseManager.onBlock(() => { + this.logger.error('⚠️ PAPI License has been BLOCKED - Interactive messages are now disabled'); + }); + + const initialStatus = licenseManager.getStatus(); + + if (initialStatus.status === 'PENDING_ACTIVATION') { + this.logger.warn( + '⏳ PAPI License is pending activation - Interactive messages will be blocked until activation', + ); + } else if (initialStatus.status === 'MACHINE_MISMATCH') { + this.logger.error('❌ PAPI License is bound to another server - Interactive messages are disabled'); + } else if (initialStatus.status === 'ACTIVE') { + this.logger.info(`✓ PAPI License is ACTIVE - Interactive messages enabled`); + } else { + this.logger.warn(`PAPI License status: ${initialStatus.status} - ${initialStatus.message}`); + } + + this.initialized = true; + } catch (error) { + this.logger.error(`Failed to initialize PAPI License Manager: ${error}`); + // Não bloqueia a inicialização, mas marca como não inicializado + } finally { + this.initializing = false; + } + } + + /** + * Check if interactive messages are allowed (buttons, lists, carousel) + * Initializes license manager on first use (lazy initialization) + * Updates instance count when checking (replaces periodic updates) + */ + public async isAllowed(instancesCount?: number): Promise { + const licenseConfig = configService.get('PAPI_LICENSE'); + + // Se não há configuração de licença, bloqueia mensagens interativas + if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) { + return false; + } + + // Inicializa se ainda não foi inicializado (lazy initialization) + await this.ensureInitialized(); + + // Se não foi inicializado após tentativa, bloqueia por segurança + if (!this.initialized) { + return false; + } + + // Atualiza contagem de instâncias quando verificar (substitui atualização periódica) + if (instancesCount !== undefined) { + licenseManager.setInstancesCount(instancesCount); + } + + return licenseManager.isAllowed(); + } + + /** + * Get current license status + * Initializes license manager on first use (lazy initialization) + */ + public async getStatus() { + const licenseConfig = configService.get('PAPI_LICENSE'); + + if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) { + return { + isValid: false, + status: 'NOT_CONFIGURED' as const, + message: 'License key and admin URL must be configured', + }; + } + + // Inicializa se ainda não foi inicializado (lazy initialization) + await this.ensureInitialized(); + + if (!this.initialized) { + return { + isValid: false, + status: 'NOT_INITIALIZED' as const, + message: 'License manager not initialized', + }; + } + + return licenseManager.getStatus(); + } + + /** + * Update instances count for heartbeat + * Called only when interactive messages are used (not periodically) + * @deprecated Use isAllowed(instancesCount) instead + */ + public setInstancesCount(count: number): void { + if (this.initialized) { + licenseManager.setInstancesCount(count); + } + } +} + +export const licenseService = new LicenseService(); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7ec1891d..ef4fbcc8 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -370,6 +370,11 @@ export type EventEmitter = { MAX_LISTENERS: number; }; +export type PapiLicense = { + KEY?: string; + ADMIN_URL?: string; +}; + export type Production = boolean; export interface Env { @@ -403,6 +408,7 @@ export interface Env { FACEBOOK: Facebook; SENTRY: Sentry; EVENT_EMITTER: EventEmitter; + PAPI_LICENSE: PapiLicense; PRODUCTION?: Production; } @@ -846,6 +852,10 @@ export class ConfigService { EVENT_EMITTER: { MAX_LISTENERS: Number.parseInt(process.env?.EVENT_EMITTER_MAX_LISTENERS) || 50, }, + PAPI_LICENSE: { + KEY: process.env?.PAPI_LICENSE_KEY, + ADMIN_URL: process.env?.PAPI_LICENSE_ADMIN_URL || 'https://padmin.intrategica.com.br/', + }, }; } } diff --git a/src/main.ts b/src/main.ts index f1f00ba9..30421e22 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,8 @@ async function bootstrap() { const prismaRepository = new PrismaRepository(configService); await prismaRepository.onModuleInit(); + // PAPI License Manager will be initialized lazily when interactive messages are first used + app.use( cors({ origin(requestOrigin, callback) { @@ -163,6 +165,9 @@ async function bootstrap() { logger.error('Error loading instances: ' + error); }); + // License manager heartbeat is handled internally by the PAPI licenseManager + // Instance count is updated only when interactive messages are used (lazy initialization) + onUnexpectedError(); } diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index 6970fd9b..3fa76379 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -448,6 +448,95 @@ export const buttonsMessageSchema: JSONSchema7 = { required: ['number'], }; +export const carouselMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + title: { type: 'string' }, + body: { type: 'string' }, + footer: { type: 'string' }, + cards: { + type: 'array', + minItems: 1, + maxItems: 10, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + footer: { type: 'string' }, + body: { type: 'string' }, + imageUrl: { type: 'string' }, + header: { + type: 'object', + properties: { + title: { type: 'string' }, + subtitle: { type: 'string' }, + imageUrl: { type: 'string' }, + videoUrl: { type: 'string' }, + }, + }, + buttons: { + type: 'array', + maxItems: 3, + items: { + type: 'object', + properties: { + id: { type: 'string' }, + title: { type: 'string' }, + displayText: { type: 'string' }, + url: { type: 'string' }, + phoneNumber: { type: 'string' }, + quickReplyButton: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + }, + urlButton: { + type: 'object', + properties: { + url: { type: 'string' }, + }, + }, + callButton: { + type: 'object', + properties: { + phoneNumber: { type: 'string' }, + }, + }, + copyCodeButton: { + type: 'object', + properties: { + code: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + delay: { + type: 'integer', + description: 'Enter a value in milliseconds', + }, + quoted: { ...quotedOptionsSchema }, + everyOne: { type: 'boolean', enum: [true, false] }, + mentioned: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"mentioned" must be an array of numeric strings', + }, + }, + }, + required: ['number', 'cards'], +}; + export const decryptPollVoteSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/tsconfig.json b/tsconfig.json index af814134..2bda6767 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,11 @@ "@exceptions": ["./src/exceptions"], "@libs/*": ["./src/libs/*"], "@utils/*": ["./src/utils/*"], - "@validate/*": ["./src/validate/*"] + "@validate/*": ["./src/validate/*"], + "baileys": ["./papi/lib"], + "baileys/*": ["./papi/lib/*"], + "baileys/lib/Types/Label": ["./papi/lib/Types/Label"], + "baileys/lib/Types/LabelAssociation": ["./papi/lib/Types/LabelAssociation"] }, "moduleResolution": "Node" }, diff --git a/tsup.config.ts b/tsup.config.ts index f09ecd87..db46d727 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,4 +1,4 @@ -import { cpSync } from 'node:fs'; +import { cpSync } from 'fs'; import { defineConfig } from 'tsup'; @@ -8,7 +8,7 @@ export default defineConfig({ splitting: false, sourcemap: true, clean: true, - minify: true, + minify: false, // Desabilitar minify pode ajudar com problemas de memória format: ['cjs', 'esm'], onSuccess: async () => { cpSync('src/utils/translations', 'dist/translations', { recursive: true });