Skip to content

Conversation

@Vitordotpy
Copy link
Contributor

@Vitordotpy Vitordotpy commented Nov 29, 2025

This Pull Request resolves critical stability issues and functional bugs in the Evolution API, focusing on Chatwoot integration robustness and support for modern WhatsApp identifiers (LID). The fixes ensure reliable message processing, prevent application crashes due to webhook errors, and normalize identifier handling across services, the fixes also ensure that messages update, delete and poll events work fine as good.

Fixes:

WhatsApp LID (Linked Identity Device) Support:
Problem: Messages originating from users with the new @lid identifier format were failing in Typebot and general message sending. The codebase aggressively stripped the domain using .split('@')[0], causing the API to lose the valid JID required for these specific identifiers.
Solution: Updated BaseChatbotService, TypebotService, and ChatwootService logic to preserve the full JID when necessary. Refactored getNumberFromRemoteJid to safely handle and clean complex JID formats (e.g., removing device suffixes like :27 while keeping the user ID intact).

Chatwoot Integration Stability (Duplicate Identifiers & Participant Errors):
Problem: The Chatwoot integration suffered from two main issues: crashes due to participantAlt being undefined when parsing message keys, and "Identifier has already been taken" (422) errors during contact creation when a contact existed but wasn't initially found.
Solution: Added safe access checks for participantAlt to prevent runtime errors. Implemented a fallback mechanism in createContact to recover the existing contact by identifier if creation fails due to duplication, ensuring the flow continues without error.

Webhook Crashes on Group Participant Updates:
Problem: The GROUP_PARTICIPANTS_UPDATE webhook event caused the application to crash when processing participant IDs that were not strings (undefined or null) in the normalizePhoneNumber function.
Solution: Added strict type coercion (String(id || '')) in WhatsappBaileysService to ensure input is always a string before processing, preventing the crash.

Unhandled Database Rejections in Chatwoot Import:
Problem: The chatwoot_import feature caused unhandled promise rejections crashing the process when the separate Postgres connection was missing or failed.
Solution: Wrapped the updateMessageSourceID and connection logic in proper try/catch blocks to gracefully handle database connectivity issues without bringing down the main application.

Messages update and delete handle
Problem: A wrong verifying logic was stopping api to send webhook events of deleted, updated and poll votes.
Solution: Enhanced and changed, cached message, update message content and poll vote verifications, now the cached message verification uses the message timestamp to ignore it, wich makes the webhook to send poll vote, message update and message delete events. The poll message type verification that makes poll update not work was removed.

Summary by Sourcery

Improve system stability and Chatwoot integration reliability by identifier handling, and error resilience.

Bug Fixes:

Enable full support for WhatsApp @lid identifiers in Typebot and Chatbot services by refining JID parsing.
Fix participantAlt undefined errors and "Identifier has already been taken" loops in Chatwoot integration.
Prevent GROUP_PARTICIPANTS_UPDATE webhook crashes by sanitizing participant ID inputs.
Handle database connection failures gracefully in Chatwoot import services to prevent unhandled rejections.
Summary by Sourcery
Improve WhatsApp and Chatwoot integrations to better handle modern JID formats, avoid crashes, and gracefully recover from external and database errors.
Restored message update, poll and message delete events to work again.

Enhancements:

Support WhatsApp LID and other JID formats end‑to‑end by preserving full remoteJid where necessary and normalizing only the domain/suffix parts.
Add a Chatwoot helper to search contacts by identifier across multiple API shapes for more robust contact resolution.

Summary by Sourcery

Improve WhatsApp and Chatwoot integrations to better handle modern identifiers, prevent crashes, and restore message lifecycle webhooks.

Bug Fixes:

  • Recover existing Chatwoot contacts when creation fails with 422 errors instead of failing the flow.
  • Avoid runtime errors when resolving group participants by checking for participantAlt before accessing it.
  • Prevent crashes on GROUP_PARTICIPANTS_UPDATE webhooks by safely normalizing potentially non-string participant IDs.
  • Restore delivery/read, update, delete, and poll vote webhooks by refining message cache checks, status defaults, and update filtering logic.
  • Avoid failures when Chatwoot import message source ID updates throw by wrapping them in error handling.

Enhancements:

  • Support WhatsApp LID and other JID formats end-to-end by preserving full remoteJid in chatbot and Typebot services instead of stripping domains.
  • Expose a Chatwoot helper to search contacts by identifier across multiple API endpoints and response shapes for more robust contact resolution.
  • Improve duplicate message update detection by caching and comparing timestamps rather than simple presence flags.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 29, 2025

Reviewer's Guide

Refactors WhatsApp identifier handling to support @lid JIDs end‑to‑end, hardens Chatwoot integration and imports against 422s and DB failures, and fixes webhook/update handling logic so updates, deletes, and participant events no longer crash or get dropped.

Sequence diagram for Chatwoot contact creation with 422 fallback

sequenceDiagram
  participant WS as BaileysStartupService
  participant CS as ChatwootService
  participant CW as ChatwootAPI

  WS->>CS: createContact(instance, jid)
  CS->>CW: POST contacts (identifier = jid)
  CW-->>CS: 422 Unprocessable Entity (identifier taken)
  alt status 422 and jid defined
    CS->>CS: logger.warn("creation failed (422)...")
    CS->>CS: findContactByIdentifier(instance, jid)
    CS->>CW: GET contacts/search?q=jid
    CW-->>CS: existing contact payload or empty
    alt contact found in search
      CS->>CS: addLabelToContact(nameInbox, contactId)
      CS-->>WS: existingContact
    else not found in search
      CS->>CW: POST contacts/filter (attribute_key = identifier)
      CW-->>CS: contactByAttr payload or empty
      alt contact found by attribute
        CS-->>WS: contactByAttr[0]
      else contact not found anywhere
        CS->>CS: logger.error("Error creating contact")
        CS-->>WS: null
      end
    end
  else non-422 error
    CS->>CS: logger.error("Error creating contact")
    CS-->>WS: null
  end
Loading

Sequence diagram for chatbot media/text sending with preserved remoteJid

sequenceDiagram
  actor U as EndUser
  participant WA as WhatsApp
  participant BSvc as BaseChatbotService
  participant TSvc as TypebotService
  participant Inst as InstanceAdapter

  U-->>WA: sends message to bot (may use @lid JID)
  WA-->>TSvc: webhook with session.remoteJid

  alt Typebot sends image/video/audio
    TSvc->>Inst: mediaMessage or audioWhatsapp
      activate TSvc
      TSvc-->>Inst: payload { number = session.remoteJid, ... }
      deactivate TSvc
  end

  alt Typebot sends list or buttons
    TSvc->>Inst: processListMessage or processButtonMessage
    TSvc->>Inst: payload { number = remoteJid, ... }
  end

  alt BaseChatbotService sends generic media
    BSvc->>Inst: audioWhatsapp or mediaMessage
    BSvc-->>Inst: payload { number = remoteJid, ... }
  end

  alt BaseChatbotService sends text
    BSvc->>Inst: textMessage
    BSvc-->>Inst: payload { number = remoteJid, ... }
  end

  Inst-->>WA: outgoing WhatsApp message
  WA-->>U: bot response delivered
Loading

Class diagram for updated identifier handling and Chatwoot integration

classDiagram
  class ChatwootService {
    +createContact(instance: InstanceDto, jid: string) Promise<any>
    +findContactByIdentifier(instance: InstanceDto, identifier: string) Promise<any>
    +findContact(instance: InstanceDto, phoneNumber: string) Promise<any>
    +getNumberFromRemoteJid(remoteJid: string) string
    +handleUpsertMessage(instance: InstanceDto, key: any, body: any) Promise<void>
    +handleImportHistory(instance: InstanceDto, key: any, chatwootMessageIds: any) Promise<void>
  }

  class BaileysStartupService {
    +handleMessagesUpsert(upsert: any, type: string) Promise<void>
    +handleMessagesUpdate(update: any, key: any) Promise<void>
    +normalizePhoneNumber(id: string~or~any) string
    -baileysCache: BaileysCache
    -instanceId: string
    -instance: any
    -configService: ConfigService
    -localChatwoot: any
  }

  class BaseChatbotService {
    <<abstract>>
    +sendMediaMessage(instance: any, remoteJid: string, mediaType: string, url: string, altText: string, settings: any) Promise<void>
    +sendTextMessage(instance: any, remoteJid: string, message: string, linkPreview: boolean, settings: any) Promise<void>
  }

  class TypebotService {
    +processListMessage(instance: any, formattedText: string, remoteJid: string) Promise<void>
    +processButtonMessage(instance: any, formattedText: string, remoteJid: string) Promise<void>
    +handleImageMessage(instance: any, session: any, message: any, settings: any) Promise<void>
    +handleVideoMessage(instance: any, session: any, message: any, settings: any) Promise<void>
    +handleAudioMessage(instance: any, session: any, message: any, settings: any) Promise<void>
  }

  class ChatwootImport {
    +updateMessageSourceID(chatwootMessageId: string, sourceId: string) Promise<void>
  }

  class BaileysCache {
    +get(key: string) Promise<any>
    +set(key: string, value: any, ttlSeconds: number) Promise<void>
  }

  class ConfigService {
    +get~Chatwoot~(key: string) Chatwoot
  }

  class Chatwoot {
    +ENABLED: boolean
  }

  ChatwootService --> ChatwootImport : uses
  ChatwootService --> ConfigService : uses
  BaileysStartupService --> BaileysCache : uses
  BaileysStartupService --> ConfigService : uses
  TypebotService --|> BaseChatbotService
Loading

File-Level Changes

Change Details Files
Improve Chatwoot contact creation robustness and add identifier-based contact lookup helpers.
  • On 422 errors during contact creation, attempt to find an existing contact by the same identifier and reuse it, including reapplying labels.
  • Introduce a findContactByIdentifier helper that searches Chatwoot contacts via search endpoint and attribute filter, handling multiple possible response shapes.
  • Ensure chatwoot_import.updateMessageSourceID is awaited and wrapped in try/catch to avoid crashing on DB connectivity or import failures.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Harden group participant handling and WhatsApp JID normalization for webhooks and Chatwoot integration, including LID support.
  • Guard access to body.key.participantAlt so it is only used when present, preventing undefined access crashes for group messages.
  • Update getNumberFromRemoteJid to return early for falsy input and to preserve full JIDs that contain '@lid', only stripping device suffixes and domains for non-LID JIDs.
  • Adjust GROUP_PARTICIPANTS_UPDATE normalization helper to coerce potentially null/undefined participant IDs to strings before splitting, preventing runtime errors.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Fix Baileys message/update webhook filtering and caching so updates, deletions, and polls are emitted correctly without deadlocks.
  • Relax the incoming message filter to stop dropping poll update messages by removing the explicit pollUpdateMessage check from the early-continue condition.
  • Invert the update.message null check so only non-message status updates are skipped instead of all message-carrying updates.
  • Change the Baileys cache value from a boolean to the messageTimestamp and only treat an update as duplicate when timestamps match, reducing false positives.
  • Adjust default status mapping from DELETED to SERVER_ACK when an unknown Baileys status is encountered in webhook payloads.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Propagate full remoteJid (including @lid) through chatbot services instead of stripping domain parts.
  • Update BaseChatbotService media and text sending helpers to pass the full remoteJid to instance methods instead of splitting on '@'.
  • Update TypebotService media, list, and button message builders to use the full remoteJid as the target number, aligning with LID-compatible downstream APIs.
src/api/integrations/chatbot/base-chatbot.service.ts
src/api/integrations/chatbot/typebot/services/typebot.service.ts

Possibly linked issues

  • #[Bug] Typebot fails to send messages to users with LID - BadRequestException jidOptions.exists false: The PR updates BaseChatbot/Typebot to preserve full remoteJid, correctly supporting LID users and fixing the reported bug.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In ChatwootService.findContactByIdentifier, consider wrapping the two API calls (contacts/search and contacts/filter) in their own try/catch or at least narrowing error handling so that a failure in one request doesn’t cause the whole lookup to throw unexpectedly.
  • In getNumberFromRemoteJid, returning the full remoteJid when it contains @lid changes the function’s effective contract from “number only” to “sometimes full JID”; it may be clearer to either rename this helper or add a separate method for full JID handling to avoid future misuse.
  • When catching errors around chatwootImport.updateMessageSourceID, logging with ${error} will typically just output the error’s string representation; using this.logger.error('Error updating Chatwoot message source ID', error) will preserve stack and structured metadata for easier debugging.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ChatwootService.findContactByIdentifier`, consider wrapping the two API calls (`contacts/search` and `contacts/filter`) in their own try/catch or at least narrowing error handling so that a failure in one request doesn’t cause the whole lookup to throw unexpectedly.
- In `getNumberFromRemoteJid`, returning the full `remoteJid` when it contains `@lid` changes the function’s effective contract from “number only” to “sometimes full JID”; it may be clearer to either rename this helper or add a separate method for full JID handling to avoid future misuse.
- When catching errors around `chatwootImport.updateMessageSourceID`, logging with `${error}` will typically just output the error’s string representation; using `this.logger.error('Error updating Chatwoot message source ID', error)` will preserve stack and structured metadata for easier debugging.

## Individual Comments

### Comment 1
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:359-361` </location>
<code_context>

       return contact;
     } catch (error) {
+      if ((error.status === 422 || error.response?.status === 422) && jid) {
+        this.logger.warn(`Contact with identifier ${jid} creation failed (422). Checking if it already exists...`);
+        const existingContact = await this.findContactByIdentifier(instance, jid);
+        if (existingContact) {
+          const contactId = existingContact.id;
+          await this.addLabelToContact(this.provider.nameInbox, contactId);
+          return existingContact;
+        }
+      }
+
       this.logger.error('Error creating contact');
       console.log(error);
       return null;
</code_context>

<issue_to_address>
**suggestion:** Consider logging the original error via the logger instead of `console.log` in this catch block.

Since you’re already logging here, it would be better to pass the error into `this.logger.error` (e.g. `this.logger.error('Error creating contact', error)`) and remove `console.log(error)`. This keeps logs consistent, captures the full error in structured logging, and avoids extra stdout noise.

```suggestion
      this.logger.error('Error creating contact', error);
      return null;
```
</issue_to_address>

### Comment 2
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:2531-2534` </location>
<code_context>
   }

   public getNumberFromRemoteJid(remoteJid: string) {
+    if (!remoteJid) {
+      return '';
+    }
+    if (remoteJid.includes('@lid')) {
+      return remoteJid;
+    }
</code_context>

<issue_to_address>
**question (bug_risk):** Returning the full `remoteJid` for LID values may be surprising given the method name `getNumberFromRemoteJid`.

This now returns the raw `remoteJid` for LIDs but a normalized number for non-LIDs, which makes the method’s behavior inconsistent with its name and with existing expectations. If we need to preserve full LID JIDs, consider either renaming this method or adding a separate helper for the LID-specific case so callers always know whether they get a bare number or a full JID.
</issue_to_address>

### Comment 3
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1456` </location>
<code_context>
         }

-        await this.baileysCache.set(updateKey, true, 30 * 60);
+        await this.baileysCache.set(updateKey, update.messageTimestamp, 30 * 60);

         if (status[update.status] === 'READ' && key.fromMe) {
</code_context>

<issue_to_address>
**issue:** Using `messageTimestamp` as the cache value without guarding `undefined` can lead to incorrect deduplication.

Because you now cache `update.messageTimestamp` and compare `update.messageTimestamp == cached.messageTimestamp`, any updates where `messageTimestamp` is `undefined` (or missing) will all share the same `undefined` cache value and incorrectly dedupe as equal. Existing cache entries that still store `true` will also read as `cached.messageTimestamp === undefined`, again causing unintended deduplication. Please either guard `messageTimestamp === undefined` (fall back to the old boolean behavior or skip timestamp-based dedupe) and/or write a new cache value shape (e.g. `{ messageTimestamp }` under a versioned key) so old boolean entries can’t be misread as timestamp-based ones.
</issue_to_address>

### Comment 4
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1611-1615` </location>
<code_context>

       // Helper to normalize participantId as phone number
-      const normalizePhoneNumber = (id: string): string => {
+      const normalizePhoneNumber = (id: string | any): string => {
         // Remove @lid, @s.whatsapp.net suffixes and extract just the number part
-        return id.split('@')[0];
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The `normalizePhoneNumber` parameter type `string | any` is misleading and undermines type safety.

`string | any` is equivalent to `any`, so this hides the actual typing and weakens type safety. Since you already handle falsy values (`String(id || '')`), consider a more accurate type like `id: string | undefined | null`, or `id: unknown` and then narrow inside the function. That keeps the contract explicit and avoids a disguised `any`.

```suggestion
      // Helper to normalize participantId as phone number
      const normalizePhoneNumber = (id: string | null | undefined): string => {
        // Remove @lid, @s.whatsapp.net suffixes and extract just the number part
        return String(id || '').split('@')[0];
      };
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…integration

Refactor  to preserve LID identifiers and update  parameter type for better type safety as per code review feedback.
@claudiocastelo
Copy link

claudiocastelo commented Nov 29, 2025

top, tenho visto relamações aumentando sobre msgs não aparecendo no chatwoot devido essas alterações do identificador lid

@Vitordotpy Vitordotpy changed the title Fix/all wrong things in this api Fix: @lid problems, messages events and chatwoot integration errors Nov 29, 2025
Vitordotpy and others added 3 commits November 30, 2025 00:25
Refactor message handling and polling updates, including decryption logic for poll votes and cache management for message updates. Improved event processing flow and added handling for various message types.
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.

2 participants