Skip to content

Conversation

@Vitordotpy
Copy link
Contributor

@Vitordotpy Vitordotpy commented Nov 28, 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.

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.

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.

Bug Fixes:

  • Handle duplicate-identifier errors in Chatwoot contact creation by looking up and reusing existing contacts instead of failing.
  • Prevent crashes when participantAlt is missing on group messages by only using it when available and falling back to participant.
  • Avoid webhook failures on GROUP_PARTICIPANTS_UPDATE events by safely normalizing possibly non-string participant IDs.
  • Prevent unhandled rejections during Chatwoot history import by wrapping message source ID updates in error handling.

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.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 28, 2025

Reviewer's Guide

Strengthens Chatwoot, Typebot, and WhatsApp Baileys integrations by preserving full JIDs (including @lid), hardening Chatwoot contact lookup and import flows against 422/database failures, and preventing webhook crashes from invalid participant IDs.

Sequence diagram for Chatwoot contact creation with 422 fallback by identifier

sequenceDiagram
  actor BotEngine
  participant ChatwootService
  participant ChatwootAPI

  BotEngine->>ChatwootService: createContact(instance, jid)
  ChatwootService->>ChatwootAPI: POST contacts (identifier = jid)
  alt contact created successfully
    ChatwootAPI-->>ChatwootService: 201 Created contact
    ChatwootService->>ChatwootAPI: POST addLabelToContact(inbox, contactId)
    ChatwootService-->>BotEngine: contact
  else 422 identifier has already been taken
    ChatwootAPI-->>ChatwootService: 422 Unprocessable Entity
    ChatwootService->>ChatwootService: check error.status and error.message
    ChatwootService->>ChatwootService: findContactByIdentifier(instance, jid)
    ChatwootService->>ChatwootAPI: GET contacts/search q=jid
    alt contact found in search payload
      ChatwootAPI-->>ChatwootService: contact.data.payload[0]
      ChatwootService->>ChatwootAPI: POST addLabelToContact(inbox, contactId)
      ChatwootService-->>BotEngine: existingContact
    else search empty
      ChatwootAPI-->>ChatwootService: empty result
      ChatwootService->>ChatwootAPI: POST contacts/filter identifier == jid
      alt contact found in filter payload
        ChatwootAPI-->>ChatwootService: contact.payload[0]
        ChatwootService->>ChatwootAPI: POST addLabelToContact(inbox, contactId)
        ChatwootService-->>BotEngine: existingContact
      else still not found
        ChatwootAPI-->>ChatwootService: empty result
        ChatwootService-->>BotEngine: null
      end
    end
  else other error
    ChatwootAPI-->>ChatwootService: error
    ChatwootService->>ChatwootService: logger.error Error creating contact
    ChatwootService-->>BotEngine: null
  end
Loading

Sequence diagram for WhatsApp GROUP_PARTICIPANTS_UPDATE normalization

sequenceDiagram
  participant WhatsAppWebhook
  participant BaileysStartupService
  participant WebhookConsumer

  WhatsAppWebhook-->>BaileysStartupService: GROUP_PARTICIPANTS_UPDATE body
  BaileysStartupService->>BaileysStartupService: for each participantId
  BaileysStartupService->>BaileysStartupService: normalizePhoneNumber(id)
  BaileysStartupService->>BaileysStartupService: String(id || '').split('@')[0]
  BaileysStartupService-->>WebhookConsumer: sanitizedParticipantId
  WebhookConsumer->>WebhookConsumer: process update without crash
Loading

Class diagram for updated chatbot and WhatsApp services JID handling

classDiagram
  class BaseChatbotService {
    <<abstract>>
    +sendMedia(instance, remoteJid, mediaType, url, altText, settings) void
    +sendText(instance, remoteJid, message, linkPreview, settings) void
  }

  class TypebotService {
    +processListMessage(instance, formattedText, remoteJid) void
    +processButtonMessage(instance, formattedText, remoteJid) void
  }

  class ChatwootService {
    +createContact(instance, jid) any
    +findContact(instance, phoneNumber) any
    +findContactByIdentifier(instance, identifier) any
    +getNumberFromRemoteJid(remoteJid) string
    +startImportHistoryMessages(instance) void
    +handleGroupMessage(body) void
  }

  class BaileysStartupService {
    +handleGroupParticipantsUpdate(body) void
    +normalizePhoneNumber(id) string
  }

  BaseChatbotService <|-- TypebotService
  class ChatwootImport {
    +updateMessageSourceID(messageId, sourceId) Promise~void~
  }

  class InstanceDto

  ChatwootService --> InstanceDto
  ChatwootService --> ChatwootImport
  TypebotService --> InstanceDto
  BaileysStartupService --> InstanceDto
Loading

File-Level Changes

Change Details Files
Make media and message sending use full remoteJid instead of stripping the domain to properly support LID identifiers.
  • Pass session.remoteJid directly as the number for Typebot media (image, video, audio) sends.
  • Use full remoteJid when constructing list and button messages in Typebot.
  • Use full remoteJid for BaseChatbotService text and media/audio sending helpers instead of remoteJid.split('@')[0].
src/api/integrations/chatbot/typebot/services/typebot.service.ts
src/api/integrations/chatbot/base-chatbot.service.ts
Improve JID/phone-number parsing and LID handling in Chatwoot and WhatsApp webhook flows.
  • Guard participantAlt access by additionally checking it exists before using it for LID-based group participant phone extraction.
  • Refine getNumberFromRemoteJid to safely handle empty input, strip device suffixes, and explicitly remove known WhatsApp domains (@s.whatsapp.net, @g.us, @lid).
  • Loosen normalizePhoneNumber signature to accept any type and coerce to string with String(id
Harden Chatwoot contact creation and lookup flows against duplicate identifier (422) errors.
  • On 422 errors indicating an identifier is already taken when creating a contact, log a warning and attempt to locate the existing contact by identifier (jid).
  • Introduce findContactByIdentifier, which first queries Chatwoot contacts/search with q, then falls back to contacts/filter by identifier, handling multiple possible response shapes, and returns the first matching contact.
  • When an existing contact is recovered after a 422, apply the inbox label and return the contact instead of failing.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Prevent Chatwoot import history from crashing the main process on database connectivity failures.
  • Wrap chatwootImport.updateMessageSourceID in a try/catch block inside updateMessage, logging any failure instead of letting it propagate and crash the process.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts

Possibly linked issues

  • #[Bug] Typebot fails to send messages to users with LID - BadRequestException jidOptions.exists false: PR preserves full JID and updates Typebot/BaseChatbot JID parsing, directly fixing the LID reply failure 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:

  • The new findContactByIdentifier method contains several near-duplicate response-shape checks (contact.data.payload, contact.payload, contactByAttr.payload, contactByAttr.data.payload); consider centralizing this into a small helper that normalizes a Chatwoot response to a single payload array to reduce branching and future maintenance overhead.
  • In getNumberFromRemoteJid, the function now guards against falsy input but the signature still declares remoteJid: string; it would be clearer to either widen the type (e.g., string | null | undefined) or move the null/undefined handling to the call sites so the type matches actual expectations.
  • The repeated inline logic for deriving rawPhoneNumber from body.key in ChatwootService (checking addressingMode === 'lid', fromMe, participantAlt, etc.) would be easier to reason about and less error-prone if factored into a shared helper that encapsulates the JID-to-phone extraction rules in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `findContactByIdentifier` method contains several near-duplicate response-shape checks (`contact.data.payload`, `contact.payload`, `contactByAttr.payload`, `contactByAttr.data.payload`); consider centralizing this into a small helper that normalizes a Chatwoot response to a single `payload` array to reduce branching and future maintenance overhead.
- In `getNumberFromRemoteJid`, the function now guards against falsy input but the signature still declares `remoteJid: string`; it would be clearer to either widen the type (e.g., `string | null | undefined`) or move the null/undefined handling to the call sites so the type matches actual expectations.
- The repeated inline logic for deriving `rawPhoneNumber` from `body.key` in ChatwootService (checking `addressingMode === 'lid'`, `fromMe`, `participantAlt`, etc.) would be easier to reason about and less error-prone if factored into a shared helper that encapsulates the JID-to-phone extraction rules in one place.

## Individual Comments

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

       return contact;
     } catch (error) {
+      if (
+        (error.status === 422 || error.response?.status === 422) &&
+        (error.message?.includes('taken') || error.response?.data?.message?.includes('taken')) &&
+        jid
+      ) {
</code_context>

<issue_to_address>
**issue (bug_risk):** Relying on `'taken'` substring in the error message is brittle and may misclassify errors.

This 422 path depends on `error.message?.includes('taken')` / `error.response?.data?.message?.includes('taken')`, which tightly couples behavior to a particular English string from Chatwoot. That can silently break with localization, wording changes, or different 422 variants. Prefer branching on a structured signal from Chatwoot (e.g., error code, attribute, or a specific payload field). If that’s not available, at least narrow this condition (e.g., to cases where the failing field is the identifier/email) so unrelated 422s containing the word `taken` don’t incorrectly hit this path.
</issue_to_address>

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

   public getNumberFromRemoteJid(remoteJid: string) {
-    return remoteJid.replace(/:\d+/, '').split('@')[0];
+    if (!remoteJid) {
+      return '';
+    }
+    return remoteJid.replace(/:\d+/, '').replace('@s.whatsapp.net', '').replace('@g.us', '').replace('@lid', '');
   }

</code_context>

<issue_to_address>
**issue:** The new `getNumberFromRemoteJid` implementation may fail to strip unknown JID suffixes and changes behavior for some inputs.

The previous `.split('@')[0]` stripped any suffix; the new version only handles three specific ones, so JIDs like `@broadcast` or future/custom domains will now include the suffix in the "number", which may break callers expecting a pure phone number. You could keep the generic stripping while still handling the numeric resource explicitly, e.g.:

```ts
if (!remoteJid) return '';
const cleaned = remoteJid.replace(/:\d+/, '');
return cleaned.split('@')[0];
```

or apply your specific replacements first and then still `split('@')[0]` as a final safeguard. Also, returning `''` instead of `undefined`/`null` for falsy `remoteJid` changes the function’s contract, so it’s worth checking existing callers for assumptions about that value.
</issue_to_address>

### Comment 3
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1616-1620` </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];
+        return String(id || '').split('@')[0];
       };

</code_context>

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

Since the implementation already does `String(id || '')`, the function is designed to handle non-string/nullable inputs. Typing the parameter as `string | any` collapses to `any` and removes useful type checking. Use a more accurate type such as `id: string | null | undefined` (or `unknown` if you truly expect arbitrary values) so call sites are still type-checked while your runtime fallback remains valid.

```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.

@Vitordotpy
Copy link
Contributor Author

Criei uma nova PR com mais fixes além desses, vou fechar essa daqui

@Vitordotpy Vitordotpy closed this Nov 29, 2025
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.

1 participant