Skip to content

fix(chatwoot): deliver bot QR when conversation is resolved#2579

Open
cesar-carlos wants to merge 1 commit into
evolution-foundation:mainfrom
cesar-carlos:fix/chatwoot-bot-qr-resolved-conversation
Open

fix(chatwoot): deliver bot QR when conversation is resolved#2579
cesar-carlos wants to merge 1 commit into
evolution-foundation:mainfrom
cesar-carlos:fix/chatwoot-bot-qr-resolved-conversation

Conversation

@cesar-carlos

@cesar-carlos cesar-carlos commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Fixes a production issue where WhatsApp QR codes are generated by Baileys but never reach the Chatwoot bot conversation after users send init.

Root cause: createBotMessage and createBotQr only post to conversations with status === 'open'. Chatwoot automations frequently resolve the bot thread (+123456) right after init, so Evolution logs conversation not found and silently drops the QR. Instances can remain stuck in connecting until QRCODE_LIMIT is reached.

Changes

  • Add getOrOpenBotConversation() to reopen the latest bot inbox conversation or create a new one before sending bot messages
  • Use the helper in createBotMessage and createBotQr
  • Match init commands exactly (init, init:<number>, iniciar) instead of includes('init')
  • Guard receiveWebhook when the WA instance is not loaded in memory
  • Send QR image + scan instructions in a single bot message (avoid duplicate QR posts per refresh)

Reproduction (before fix)

  1. Enable CHATWOOT_BOT_CONTACT=true
  2. Create/connect instance with Chatwoot integration
  3. Send init in bot conversation (+123456)
  4. Let Chatwoot automation resolve the conversation
  5. Baileys generates QR → Evolution logs WARN conversation not found → user never receives QR in Chatwoot

Test plan

  • Send init in bot conversation with a resolved thread → conversation reopens and QR image arrives
  • Send init when no bot conversation exists → conversation is created and QR arrives
  • Commands init:5511999999999 and iniciar still trigger connection
  • Commands like definition no longer trigger connection
  • QR limit message is delivered when QRCODE_LIMIT is reached
  • Normal Chatwoot ↔ WhatsApp customer conversations are unaffected

Made with Cursor

Summary by Sourcery

Ensure Chatwoot bot conversations are reopened or created before delivering QR and status messages, and tighten handling of bot init commands and WhatsApp instance availability.

Bug Fixes:

  • Prevent WhatsApp QR codes from being dropped when the bot conversation has been auto-resolved by reopening or creating the bot inbox conversation before sending messages.
  • Avoid failures when Chatwoot contact payloads lack a top-level ID by safely deriving the contact ID from nested payload data.
  • Stop triggering bot initialization on messages that merely contain the substring 'init' by matching specific init-style commands only.
  • Prevent processing of bot webhooks when the corresponding WhatsApp instance is not loaded in memory.

Enhancements:

  • Send WhatsApp QR images and scan instructions in a single bot message to avoid duplicate QR posts per refresh and improve user experience.

Reopen or create the bot inbox conversation before posting QR/status
messages, since Chatwoot automations often resolve the init thread.
Also tighten init command matching and send QR instructions in a single
bot message.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Reopens or creates a dedicated Chatwoot bot conversation before sending WhatsApp QR/status messages, hardens command parsing and webhook handling, and consolidates QR delivery into a single bot message.

Sequence diagram for reopening or creating bot conversation before QR delivery

sequenceDiagram
  actor User
  participant Chatwoot
  participant ChatwootService
  participant ChatwootClient

  User->>Chatwoot: send /init
  Chatwoot->>ChatwootService: receiveWebhook(body)
  ChatwootService->>ChatwootService: isBotInitCommand(command)
  alt [command is init]
    ChatwootService->>ChatwootService: getOrOpenBotConversation(instance, inbox, contact)
    alt [open conversation exists]
      ChatwootService-->>ChatwootService: return openConversation
    else [no open conversation]
      ChatwootService->>ChatwootClient: contacts.listConversations(accountId, contactId)
      ChatwootClient-->>ChatwootService: conversations
      alt [latest inbox conversation is resolved]
        ChatwootService->>ChatwootClient: conversations.toggleStatus(accountId, conversationId, status)
        ChatwootClient-->>ChatwootService: updatedConversation
      else [no conversation]
        ChatwootService->>ChatwootClient: conversations.create(accountId, contact_id, inbox_id)
        ChatwootClient-->>ChatwootService: createdConversation
      end
    end
    ChatwootService->>ChatwootService: createBotQr(instance, msgQrCode, messageType, fileStream, filename)
  end
Loading

File-Level Changes

Change Details Files
Ensure bot QR/status messages always target an open bot conversation
  • Add getOrOpenBotConversation to reuse, reopen, or create the bot inbox conversation before sending messages
  • Reuse contact ID resolution logic and guard when contact or Chatwoot client is missing
  • Use the new helper in bot message/QR creation instead of relying solely on open conversations
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Tighten bot init command detection and protect webhook processing when WA instance is missing
  • Introduce isBotInitCommand to match exact init-style commands rather than substring matches
  • Guard receiveWebhook early when the WhatsApp instance is not loaded in memory
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Send QR image and instructions in a single bot message
  • Remove separate bot QR notification before the text message
  • Change createBotMessage call to createBotQr so QR image and instructions are sent together, avoiding duplicate QR posts
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts

Possibly linked issues

  • #META Chatwoot umbrella: PR fixes missing bot QR delivery to Chatwoot, one of the QR/connection problems tracked in this meta-issue.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In getOrOpenBotConversation you re-query contacts.listConversations even though getOpenConversationByContact already fetched the conversations; consider refactoring getOpenConversationByContact to optionally return all conversations (or accept a pre-fetched list) to avoid duplicate API calls on every bot message/QR.
  • The logic to derive contactId from contact.id || (contact as any)?.payload?.contact?.id is now duplicated in multiple methods; extracting a small helper (e.g. getContactId(contact)) would reduce repetition and keep the null-handling consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `getOrOpenBotConversation` you re-query `contacts.listConversations` even though `getOpenConversationByContact` already fetched the conversations; consider refactoring `getOpenConversationByContact` to optionally return all conversations (or accept a pre-fetched list) to avoid duplicate API calls on every bot message/QR.
- The logic to derive `contactId` from `contact.id || (contact as any)?.payload?.contact?.id` is now duplicated in multiple methods; extracting a small helper (e.g. `getContactId(contact)`) would reduce repetition and keep the null-handling consistent.

## Individual Comments

### Comment 1
<location path="src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts" line_range="982-983" />
<code_context>
       return null;
     }

+    const contactId = contact.id || (contact as any)?.payload?.contact?.id;
+    if (!contactId) {
+      this.logger.warn('contact id not found');
+      return null;
</code_context>
<issue_to_address>
**suggestion:** Consider extracting contactId resolution into a small helper to avoid duplication.

This `contactId` resolution is duplicated with `getOrOpenBotConversation`. A shared helper (e.g. `getContactId(contact)`) would keep the behavior consistent and easier to update if the Chatwoot payload shape changes.

Suggested implementation:

```typescript
    const contactId = this.getContactId(contact);
    if (!contactId) {
      this.logger.warn('contact id not found');
      return null;
    }

```

To fully implement the refactor and avoid duplication:

1. Inside the `ChatwootService` class, add a helper method (placement: near other private helpers):

```ts
  private getContactId(contact: any): string | null {
    const contactId = contact?.id || (contact as any)?.payload?.contact?.id;
    return contactId ?? null;
  }
```

2. In the `getOrOpenBotConversation` method (where the same `contactId` resolution is currently duplicated), replace the inline logic:

```ts
    const contactId = contact.id || (contact as any)?.payload?.contact?.id;
```

with:

```ts
    const contactId = this.getContactId(contact);
```

This will centralize the payload shape knowledge in one place and keep behavior consistent across usages.
</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.

Comment on lines +982 to +983
const contactId = contact.id || (contact as any)?.payload?.contact?.id;
if (!contactId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Consider extracting contactId resolution into a small helper to avoid duplication.

This contactId resolution is duplicated with getOrOpenBotConversation. A shared helper (e.g. getContactId(contact)) would keep the behavior consistent and easier to update if the Chatwoot payload shape changes.

Suggested implementation:

    const contactId = this.getContactId(contact);
    if (!contactId) {
      this.logger.warn('contact id not found');
      return null;
    }

To fully implement the refactor and avoid duplication:

  1. Inside the ChatwootService class, add a helper method (placement: near other private helpers):
  private getContactId(contact: any): string | null {
    const contactId = contact?.id || (contact as any)?.payload?.contact?.id;
    return contactId ?? null;
  }
  1. In the getOrOpenBotConversation method (where the same contactId resolution is currently duplicated), replace the inline logic:
    const contactId = contact.id || (contact as any)?.payload?.contact?.id;

with:

    const contactId = this.getContactId(contact);

This will centralize the payload shape knowledge in one place and keep behavior consistent across usages.

@cesar-carlos

Copy link
Copy Markdown
Author

Related issue: #2580

@dpaes

dpaes commented Jun 12, 2026

Copy link
Copy Markdown

please, do not send to main, you need to send to develop

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