Skip to content

fix(meta): handle message_echoes and guard missing contact fields#1

Open
ricardoisus wants to merge 128 commits into
mainfrom
fix/cloudapi-message-echoes
Open

fix(meta): handle message_echoes and guard missing contact fields#1
ricardoisus wants to merge 128 commits into
mainfrom
fix/cloudapi-message-echoes

Conversation

@ricardoisus
Copy link
Copy Markdown
Owner

Contexto

Em produção, mensagens enviadas fora da Evolution (app oficial WhatsApp) chegam no webhook Meta como message_echoes/smb_message_echoes, mas o parser atual espera apenas o formato clássico messages[] e pode quebrar com erros como reading '0' e reading 'name'.

Comportamento esperado

  • Webhooks com message_echoes/smb_message_echoes devem entrar no mesmo fluxo de persistência/roteamento de mensagens.
  • Campos opcionais de contato ausentes não devem quebrar o processamento.
  • remoteJid deve ser resolvido de forma robusta para mensagens e statuses.

Alterações

  • Normalização de message_echoes/smb_message_echoes para o pipeline padrão de mensagens.
  • Guardas para profile/name/phone opcionais.
  • Cálculo resiliente de remoteJid.
  • Proteção no connectToWhatsapp para payloads fora do formato clássico.

Validação executada

  • DATABASE_PROVIDER=postgresql npm run db:generate
  • npm run build
  • Inspeção de logs e fluxo de persistência para payloads message_echoes.

Teste manual sugerido

  1. Enviar mensagem pelo app oficial WhatsApp (fora da Evolution).
  2. Confirmar recebimento no webhook /webhook/meta com HTTP 200.
  3. Confirmar ausência de TypeError em runtime.
  4. Validar persistência da mensagem (wamid) e publicação do evento no RabbitMQ.

caiobleggi and others added 30 commits December 9, 2025 12:03
Introduces a new API endpoint and supporting logic to decrypt WhatsApp poll votes. Adds DecryptPollVoteDto, validation schema, controller method, and service logic to process and aggregate poll vote results based on poll creation message key.
Updated DecryptPollVoteDto to use a nested message.key structure and moved remoteJid to the top level. Adjusted the controller and validation schema to match the new structure for consistency and clarity.
feat(channel): add support for @newsletter in sendMessage and findChannels
…elop

Feature: Endpoint para Descriptografar e Visualizar Votos de Enquetes
…tejid-wrong-format

fix(baileys): normalize remote JIDs for consistent database lookups
…tejid-normalization-and-cache-race

fix: normalize remoteJid in message updates and handle race condition in contact cache
- Introduced a flag to prevent reconnection during instance deletion.
- Improved logging for connection updates and errors during logout.
- Added a delay before reconnection attempts to avoid rapid loops.
- Enhanced webhook headers for better tracking and debugging.
- Updated configuration to support manual Baileys version setting.
Creates new migration to ensure lid column exists even in databases
where it was previously dropped by the Kafka integration migration.

Uses prepared statement to check column existence before adding,
ensuring compatibility with both fresh and existing installations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add audio-decode library for audio buffer analysis
- Implement getAudioDuration() to extract duration from audio
- Implement getAudioWaveform() to generate 64-value waveform array
- Normalize waveform values to 0-100 range for WhatsApp compatibility
- Change audio bitrate from 128k to 48k per WhatsApp PTT requirements
- Add Baileys patch to prevent waveform overwrite
- Increase Node.js heap size for build to prevent OOM

Fixes evolution-foundation#1086
…preview

- Fix linkPreview logic in Baileys to default to true
- Add support for 'file' and 'embed' types in Typebot integration
- Ensure correct media type detection for PDFs and docs
…ng, Typebot integration, and comprehensive API functionalities for chat, group, and business profiles.
…tions, messages, groups, profiles, and integrating with Chatwoot, OpenAI, and S3.
octo-patch and others added 29 commits May 6, 2026 13:49
…lution-foundation#2495) (evolution-foundation#2515)

The `getLastMessage` function was using an invalid Prisma where-clause
syntax `{ key: { remoteJid: number } }` for a JSON/JSONB field. Prisma
requires `{ key: { path: ['remoteJid'], equals: value } }` to filter on
a JSON field path. This caused a PrismaClientValidationError whenever
`archiveChat` or `markChatUnread` called `getLastMessage` with a chat
number instead of providing lastMessage directly.

Co-authored-by: octo-patch <octo-patch@github.com>
…pes (evolution-foundation#2418)

The condition used isPnUser() which only matches @pn.whatsapp.net JIDs,
silently excluding normal users (@s.whatsapp.net) and LID users (@lid).
Messages were never actually marked as read for these JID types.

Replace allowlist approach (isJidGroup || isPnUser) with denylist
(!isJidBroadcast && !isJidNewsletter) to correctly include all valid
user and group JIDs while excluding only broadcast and newsletter JIDs.

Fixes evolution-foundation#2277
… (zombie cleanup) (evolution-foundation#2520)

* fix(instance.controller): emit remove.instance even when logout fails

When a Baileys WebSocket dies but the in-memory `waInstances[name]`
entry still exists (a "zombie" instance), `deleteInstance()` calls
`await this.logout()` which throws "Connection Closed". The throw
causes the outer try/catch to skip the `eventEmitter.emit('remove.instance')`
call — which is the only mechanism that purges the zombie from
`waInstances`.

Result: zombies persist in memory until the entire `evo2_api`
container is restarted, affecting ALL instances on the host (not
just the broken one). Operators have no per-instance recovery path
in v2.3.x — their only option is `docker restart`, which forces
every connected user to re-scan the QR code.

Fix: wrap the inner `logout()` call in its own try/catch. Log a
warning when it fails but continue to the cleanup emit. The
in-memory entry must be removed regardless of whether logout
completed cleanly — `remove.instance` is the canonical way to
purge a stuck instance, and DB/cache cleanup happens in the same
event handler.

This makes `DELETE /instance/:name` idempotent against zombies: a
caller can always recover a single instance without nuking the
whole host.

Refs:
- evolution-foundation#693  (instance/restart closes the session)
- evolution-foundation#1286 (Connection Closed in v2.2.3)
- evolution-foundation#2026 (Sync lost after reboot)
- evolution-foundation#2027 (Loss of synchronization on reboot)

Tested in production at Rigarr (14 instances, ~25k msgs/day) by
overlaying this patch on v2.3.7 via Docker. Before: any zombie
forced a full container restart. After: per-instance cleanup
works cleanly while other vendors stay connected.

Signed-off-by: Bruno Cavalcante Sgarbi <bcsgarbi@gmail.com>

* review: address Sourcery feedback — neutral language + log error object

Per evolution-foundation#2520 review:

1. Drop vendor-specific markers in code comment and log message
   (was '[ZOMBIE-CLEANUP]' and 'RIGARR PATCH'). Comment now describes
   the bug in upstream-friendly terms.

2. Pass the full error object to logger.warn instead of toString(),
   following the existing convention in monitor.service.ts
   ('no.connection' handler) where structured object logging is used
   to preserve diagnostic detail.

No behavior change.

---------

Signed-off-by: Bruno Cavalcante Sgarbi <bcsgarbi@gmail.com>
Co-authored-by: Bruno Sgarbi <bcsgarbi@gmail.com>
…-foundation#2470)

- Changed truthy check to strict equality (=== true) to properly handle
  string values like "false" sent by clients (e.g., n8n)
- Fixed validation schema property name from 'everyOne' to 'mentionsEveryOne'
  to match DTO and service code

Fixes evolution-foundation#2431

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
… destructive JSON cloning (evolution-foundation#2461)

* fix(whatsapp): resolve this.isZero error in list messages by removing destructive JSON cloning

* refactor: extract listType normalization into a helper function
…on (evolution-foundation#2493)

The BusinessStartupService shared a mutable this.phoneNumber property
across concurrent webhook requests. When two webhooks arrived near-
simultaneously for the same phone_number_id, the second request could
overwrite phoneNumber before the first finished processing, causing
messages to be attributed to the wrong sender (wrong remoteJid).

Changes:
- Compute senderJid as a local variable before calling eventHandler
- Pass senderJid as parameter through eventHandler -> messageHandle
- Await eventHandler to prevent concurrent mutation
- Replace all this.phoneNumber reads inside messageHandle with the
  local senderJid parameter

Made-with: Cursor
…on-foundation#2519)

Adds POST /chat/markMessageAsPlayed/{instance} for marking received audio
messages as played (blue microphone in WhatsApp), mirroring the existing
markMessageAsRead pattern.

Baileys natively supports sock.sendReceipts(keys, 'played') but Evolution
only exposed the 'read' type via /chat/markMessageAsRead. CRMs that play
back voice notes received from contacts had no way to send the played
ack — this endpoint fills the gap with the same DTO/schema shape (key
shape: id, fromMe, remoteJid) under a 'playedMessages' array.

Mirrors:
- DTO: MarkMessageAsPlayedDto extends Key array (mirrors ReadMessageDto)
- Schema: markMessageAsPlayedSchema (JSONSchema7, mirrors readMessageSchema)
- Service: markMessageAsPlayed -> client.sendReceipts(keys, 'played')
- Controller: markMessageAsPlayed -> waMonitor delegation
- Router: POST routerPath('markMessageAsPlayed')

Use case: agent CRMs (Chatwoot-like) that present audio messages with a
play button and need to send the played receipt back to the contact when
the agent plays the audio in the dashboard.
…oundation#2510)

* fix(history-sync): emit completion before contact upsert

* chore(ci): rerun checks on current head

* fix(history-sync): include progress in completion event

* style(chatwoot): fix lint violations

* fix(history-sync): ignore non-primary completions

* Revert "fix(history-sync): ignore non-primary completions"

This reverts commit c0523d6.

* chore: add ghcr publish workflow to branch
…tion Foundation 2026

- Apache 2.0 license with © 2026 Evolution Foundation
- Standardized README structure (header, badges, footer)
- Unified contact: suporte@evofoundation.com.br
- Unified URLs: evolutionfoundation.com.br + docs.evolutionfoundation.com.br
- Added/updated NOTICE, TRADEMARKS.md, SECURITY.md, CONTRIBUTING.md
- Canonical cover image at public/hover-evolution.png

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n-foundation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

* feat(group): add updateMemberAddMode endpoint

Expose the WhatsApp protocol's group "member add mode" setting
(supported by Baileys via groupMemberAddMode) as a new endpoint, so
clients can toggle whether non-admin members are allowed to add
participants to a group.

POST /group/updateMemberAddMode/{instance}?groupJid=<jid>
body: { "mode": "admin_add" | "all_member_add" }

- admin_add       → only admins can add new members
- all_member_add  → any member can add new members (WhatsApp default)

Mirrors the existing pattern of updateSetting:
- DTO  : GroupUpdateMemberAddModeDto
- Schema: updateMemberAddModeSchema (validates mode enum)
- Service: WhatsappBaileysService.updateMemberAddMode
- Controller: GroupController.updateMemberAddMode
- Route: POST /group/updateMemberAddMode

No breaking change. No new dependency.

* ci: build & publish fork Docker image to GHCR

Triggered on every push to feat/group-member-add-mode. Produces:
  ghcr.io/hassankaid/evolution-api:member-add-mode
  ghcr.io/hassankaid/evolution-api:sha-<commit>

Allows users running self-hosted Evolution to swap their Docker image
to this fork while keeping all their state (sessions, DB, Redis)
untouched, until the upstream PR evolution-foundation#2525 is merged.
…dation#2530)

* feat(licensing): mirror evolution-go license activation system

Phase 1 of the licensing rollout — backend only. Brings the same
activation lifecycle that already exists in evolution-go (pkg/core)
into evolution-api as src/licensing/, plus a public /license/* router
and a gate middleware that 503s API traffic until activation.

Module layout (mirrors pkg/core/*.go):
- src/licensing/model.ts       (config keys, type contracts)
- src/licensing/store.ts       (Prisma RuntimeConfig CRUD + hardware-based instance ID)
- src/licensing/endpoint.ts    (XOR-decoded URL with parts-array dev fallback)
- src/licensing/transport.ts   (axios + HMAC-SHA256 signing)
- src/licensing/integrity.ts   (paridade-only stubs - Baileys does not consume)
- src/licensing/runtime.ts     (RuntimeContext, initializeRuntime, gateMiddleware,
                                heartbeat, shutdown, completeActivation)

Public routes (no auth) - same contract as evolution-go:
- GET /license/status                 -> {status, instance_id, api_key (masked)}
- GET /license/register?redirect_uri  -> POST /v1/register/init upstream
- GET /license/activate?code=         -> POST /v1/register/exchange + activate

Bootstrap order in src/main.ts:
1. setDB(prisma)
2. initializeRuntime({tier: 'evolution-api', version, globalApiKey})
3. /license router (always public)
4. gateMiddleware (503 LICENSE_REQUIRED before business routers)
5. business routers
6. startHeartbeat (30 min, fire-and-forget)
7. SIGTERM/SIGINT -> POST /v1/deactivate (best-effort)

Behaviour notes:
- AUTHENTICATION_API_KEY is reused as bootstrap key (mirrors GLOBAL_API_KEY in Go).
  If a license already exists in the DB, the service runs locally even if the
  licensing server is unreachable.
- Gate middleware allowlist: /license/*, /manager/**, /assets/**, /store/**,
  /health, /server/ok, /favicon.ico, /ws, common static extensions.
- Heartbeat carries optional telemetry_bundle with messages_sent / messages_recv
  that callers can feed via trackMessageSent() / trackMessageRecv().

Schema:
- New Prisma model RuntimeConfig (key/value) on both postgresql and mysql schemas.
  Run npm run db:migrate:dev per provider before starting the service.

Endpoint URL ofuscation:
- Set LICENSE_ENDPOINT_ENCODED + LICENSE_ENDPOINT_XOR_KEY (hex) in release builds
  to avoid the licensing URL appearing as a plain string in the bundle.
- Dev fallback assembles license.evolutionfoundation.com.br from a parts array,
  same technique as evolution-go.

Phase 2 (manager-v2 UI for the activation flow) lands in a separate PR
under evolution-foundation/evolution-manager-v2.

* feat(licensing): add Prisma migration for RuntimeConfig table

Adds the database migration that creates the licensing storage table
(postgres + mysql). This was missing from the previous licensing commit.
Without this migration, npm run db:deploy is a no-op and the server
will fail to find the table at boot.

* release: 2.4.0 - license activation required

Polishes the licensing rollout for public release:

- Better error UX: HTTP 503 now carries instance_id, docs_url and an
  actionable message instructing the operator to open the manager UI
  or set AUTHENTICATION_API_KEY in .env.
- Better boot banner: lists the activation paths (manager UI, env var)
  with the docs URL and the instance_id.
- Auto-detect missing migration: if the RuntimeConfig table is absent,
  the server prints a clear banner asking the operator to run
  npm run db:deploy and exits 1, instead of throwing a Prisma stack
  trace from inside the bootstrap.
- Version bump 2.3.7 -> 2.4.0.
- CHANGELOG entry with BREAKING CHANGE notice and migration guide.
- README section 'License Activation' linking to
  docs.evolutionfoundation.com.br/licensing.

* build(manager): refresh manager/dist with v2.4.0 bundle

- Bumps the embedded manager UI to the version published on
  evolution-foundation/evolution-manager-v2 main, which now includes
  the license-aware login flow that mirrors evolution-go-manager.
- Removes the legacy manager/dist/assets/test-interactive.js stand-alone
  script — its functionality is now a proper React component
  (TestInteractiveModal) inside the bundle, accessed from the instance
  card on the dashboard.
- Updates the manager-v2 submodule pointer to track main.

* style(licensing): apply prettier/eslint autofix and hoist DOCS_URL

The autofix from the pre-push hook reorders imports, normalizes line
breaks and reformats the constructor signature. Also moves DOCS_URL to
the top of the module so the auto-detect error path can reference it
without hitting the temporal dead zone.

* feat(licensing): bake licensing endpoint into bundle at build time

Mirrors evolution-go/tools/build-dist/obfuscate.go: the URL of the licensing
server is now XOR-encoded into the JS bundle by tsup `define`, so it never
appears as a plain literal in dist/main.js. The Dockerfile accepts the pair
as build-args (NOT runtime env vars) so an operator cannot point the running
service at a different licensing server.

- src/licensing/endpoint.ts: read from compile-time `__LICENSE_ENDPOINT_*__`
  identifiers replaced by tsup; keep parts-array fallback for dev builds.
- tsup.config.ts: `define` reads LICENSE_ENDPOINT_ENCODED / _XOR_KEY from
  build env at the moment npm run build is invoked.
- tools/encode-url.js: helper to generate the hex pair for a given URL.
  Usage: eval "$(node tools/encode-url.js <url>)".
- Dockerfile: ARG + ENV plumbing for the build stage only.
- CHANGELOG: notes about the build-time obfuscation.

* chore: drop evolution-manager-v2 submodule

The manager-v2 source repository is now private, so the CI checkout step
fails when trying to fetch the submodule (no PAT configured, GITHUB_TOKEN
has no cross-repo read scope). Drop the submodule entirely — the runtime
artefact already lives under manager/dist/ in this repo, which is what
the Express server serves. Source for the manager continues to be
maintained at evolution-foundation/evolution-manager-v2 (private).

* style(licensing): prettier autofix on endpoint.ts

* docs(changelog): expand 2.4.0 entry with all features since 2.3.7

Previous entry only covered the licensing rollout. The release actually
includes 50 commits worth of work:

- Manager v2 completely redesigned (Tailwind v4 + @evoapi/design-system,
  dual-provider support, advanced sessions panels, license flow,
  Test Interactive modal, full i18n).
- Carousel message endpoint (POST /message/sendCarousel).
- Cross-client fix for buttons and list rendering on WhatsApp
  Web/Desktop/iOS via the <biz> stanza node and the legacy listMessage
  payload.
- Interactive buttons via deviceSentMessage with corrected CTA limits
  and PIX payment_info support.
- Catalog orderMessage and quoted productMessage support.
- New messaging-history.set event with cumulative counts.
- markMessageAsPlayed audio receipt endpoint.
- SQS custom base_url.
- LID -> phone-number mapping with cache.
- Multiple bug fixes (mentionsEveryOne, getLastMessage, markMessageAsRead,
  list-message JSON cloning, Cloud API race conditions, instance logout
  idempotency, zombie-instance cleanup, network family timeout, etc.).
…n/fix/logout-baileys

fix: logout instance
AxiosHeaderValue widened to include number; explicit String() cast keeps
runtime behavior and unblocks tsc --noEmit.
When EVOLUTION_OPERATOR_EMAIL is set in .env, initializeRuntime silently
calls the licensing server's /v1/register/auto endpoint on startup,
persists the returned api_key and activates the instance — skipping
the browser registration flow entirely.

Falls back to the manual registration flow on any failure (email not
yet registered, server unreachable, key suspended, etc.). Non-fatal.

Requires one prior manual registration so the email is known server-side.
…volution-foundation#2544)

After WhatsApp's LID (Linked Identity) migration, DMs to contacts
that resolve to @lid JIDs fail with a BadRequestException because
onWhatsApp returns exists:false for LID-based identifiers.

Add @lid to the whitelist alongside @broadcast in both
sendMessageWithTyping and sendPresence so these contacts are not
rejected by the existence check.

Co-authored-by: CORREIA Eduardo <eduardo.correia@hexagon.com>
…olution-foundation#2485) (evolution-foundation#2516)

The `audioWhatsapp` method was not forwarding `data.quoted` to
`sendMessageWithTyping`, so audio messages sent with a `quoted` key
were delivered as standalone messages instead of replies.

Added `quoted: data?.quoted` to the options object in both
`sendMessageWithTyping` call sites inside `audioWhatsapp` (the
encoding path and the direct path), matching the pattern used by all
other message-sending methods (textMessage, pollMessage, etc.).

Co-authored-by: octo-patch <octo-patch@github.com>
…ose' state (evolution-foundation#2420)

Evolution is a webhook-based integration that should always be considered
'open' since it passively receives events. Three changes fix the issue:

1. monitor.service.ts: Always call connectToWhatsapp() for Evolution
   instances on server restart, regardless of stored connection status.
2. instance.controller.ts: Call connectToWhatsapp() for Evolution
   instances during creation to initialize Chatwoot settings.
3. evolution.channel.service.ts: Restore stateConnection to 'open' and
   persist it to DB when connectToWhatsapp() is called in init mode.

Closes evolution-foundation#2419

https://claude.ai/code/session_013wxk4cj5U2H5fr2cxCgWdH

Co-authored-by: Claude <noreply@anthropic.com>
…ion#2546)

Prevents instances with trailing/leading whitespace in their names
from becoming undeletable via the API or UI. Fixes evolution-foundation#2543.
…foundation#2540)

* feat: add gifPlayback support for video messages

Allow gifPlayback and gifAttribution options to be passed to Baileys video messages.

* feat: add gif options to media DTOs

Add gifPlayback and gifAttribution properties to media DTO definitions.

* feat: add gif fields to media message schema

Add gifPlayback and gifAttribution validation support to media message schema.

* fix: validate gifAttribution values before assignment

Prevent invalid gifAttribution values from reaching Baileys media payload.
Stabilization release on top of 2.4.0-rc1. No DB or HTTP contract changes —
fixes in WhatsApp/Baileys (@lid bypass, audio quoted, instanceName trim) and
Evolution Channel (instances stuck in close), native GIF send support, and
optional headless licensing auto-activation via EVOLUTION_OPERATOR_EMAIL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ricardoisus ricardoisus force-pushed the fix/cloudapi-message-echoes branch from 532bc1d to efbedab Compare May 19, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.