Skip to content

Pr1705 #1713

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 38 commits into from
Closed

Pr1705 #1713

wants to merge 38 commits into from

Conversation

alkimimcitty
Copy link

@alkimimcitty alkimimcitty commented Jul 11, 2025

Summary by Sourcery

Improve media handling robustness, normalize LID contact identifiers, expose unread message counts, and prepare release v2.3.1

New Features:

  • Use phone number as messageId for EvoAI messages
  • Add contact merging support in Chatwoot service
  • Expose unreadMessages count in channel responses

Bug Fixes:

  • Skip messages with decryption errors or invalid stub parameters
  • Ensure S3 uploads only proceed for valid media content across integrations

Enhancements:

  • Add fallback downloadContentFromMessage in Baileys media download
  • Introduce hasValidMediaContent check and mapMediaType method to validate and map media types
  • Normalize remoteJid rewriting for '@lid' sessions in Baileys, Business, and Evolution services

Build:

  • Bump Dockerfile to v2.3.1, use npm ci, and include tsup.config.ts
  • Update package.json to v2.3.1 and upgrade dependencies (jimp, sharp, swagger-ui-express)

Deployment:

  • Update Docker Swarm image tag to evoapicloud/evolution-api:v2.3.1

Documentation:

  • Update CHANGELOG for v2.3.1

Chores:

  • Fix Typebot controller to use instanceName and bind formatting callback
  • Refine Jimp image resizing call signature in Chatwoot service

skarious and others added 30 commits May 29, 2025 14:05
CONFIG_SESSION_PHONE_VERSION updated
CONFIG_SESSION_PHONE_VERSION Updated
Update EvolutionApi DockerHub Repository and Delete Config Session Variable
…ssar-mensagens-com-variáveis

Fix Typebot formatting function
fix: corrige versão inválida do swagger-ui-express
Update Dockerhub Repository and Delete Config Session Variable
…age-upsert

feat: Adiciona mesclagem de contatos @lid no Chatwoot
Correção do envio de variáveis pelo typeboy
add unreadMessages in the response
🐛 fix: Phone number as message ID for Evo AI #ISSUE 28
KokeroO and others added 8 commits June 26, 2025 11:42
Corrige ref. instance typebot.controller.ts
- Implement broken event checking before duplicate message checking. (Do not process failed events).
- Implement error handling when downloading media with a fallback mechanism.
 - [Baileys] Trocar @LIDS em remoteJid por senderPn em todos os serviços;
 - [Baileys] Adicionar valor @lid recebido em remoteJid para previousRemoteJid (Posteriormente utilizasse em ChatwootService);
 - Minors fixes;
Copy link
Contributor

sourcery-ai bot commented Jul 11, 2025

Reviewer's Guide

This PR upgrades the WhatsApp Baileys integration to the latest API types, hardens media handling with new validation/fallback logic, refines LID‐based JID remapping and contact merging, and bumps package and Docker images to version 2.3.1.

Sequence diagram for media message upload with validation and fallback

sequenceDiagram
    participant Service as BaileysStartupService
    participant S3 as s3Service
    participant Prisma as prismaRepository
    participant Logger as logger
    participant Baileys as Baileys API
    participant Fallback as downloadContentFromMessage

    Service->>Service: hasValidMediaContent(message)
    alt No valid media
        Service->>Logger: warn('Message detected as media but contains no valid media content')
        Service-->>Service: Skip upload
    else Valid media
        Service->>Service: getBase64FromMediaMessage()
        alt DownloadMediaMessage fails
            Service->>Logger: error('Download Media failed, trying to retry in 5 seconds...')
            Service->>Fallback: downloadContentFromMessage()
            alt Fallback fails
                Service->>Logger: error('Download Media with downloadContentFromMessage also failed!')
            else Fallback succeeds
                Service->>Logger: info('Download Media with downloadContentFromMessage was successful!')
            end
        else DownloadMediaMessage succeeds
        end
        Service->>S3: uploadFile(fullName, buffer, size, {Content-Type})
        Service->>Prisma: media.create({ ... })
        Service->>S3: getObjectUrl(fullName)
        Service->>Prisma: message.update({ ... })
    end
Loading

Sequence diagram for LID-based JID remapping and contact merging in ChatwootService

sequenceDiagram
    participant Service as ChatwootService
    participant Instance as InstanceDto
    participant Chatwoot as chatwootRequest
    participant Logger as logger

    Service->>Service: createConversation(instance, body)
    alt isLid and senderPn !== previousRemoteJid
        Service->>Service: findContact(instance, remoteJid)
        alt contact.identifier !== senderPn
            Service->>Service: updateContact(instance, contact.id, ...)
            alt updateContact === null
                Service->>Service: findContact(instance, senderPn)
                alt baseContact exists
                    Service->>Service: mergeContacts(baseContact.id, contact.id)
                    Service->>Logger: verbose('Merge contacts ...')
                end
            end
        end
    end
Loading

Class diagram for BaileysStartupService media handling changes

classDiagram
    class BaileysStartupService {
        +hasValidMediaContent(message): boolean
        +mapMediaType(mediaType): Promise<string|null>
        +getBase64FromMediaMessage(data, getBuffer): Promise<any>
    }
    BaileysStartupService --|> ChannelStartupService
Loading

Class diagram for ChatwootService contact merging changes

classDiagram
    class ChatwootService {
        -mergeContacts(baseId: number, mergeId: number)
        +createConversation(instance: InstanceDto, body: any)
    }
Loading

Class diagram for unreadMessages addition in ChannelStartupService

classDiagram
    class ChannelStartupService {
        +hasValidMediaContent(message): boolean
    }
Loading

Class diagram for EvoaiService messageId logic change

classDiagram
    class EvoaiService {
        +sendMessage(...)
        -messageId = msg?.key?.id || uuidv4()
        +messageId = remoteJid.split('@')[0] || uuidv4()
    }
Loading

File-Level Changes

Change Details Files
Refactored WhatsApp message handlers to use new Baileys types and remap LID JIDs
  • Replaced IWebMessageInfo with WAMessage/WAMessageKey across sync, upsert and update handlers
  • Remapped remoteJid to senderPn for '@lid' JIDs and stored previousRemoteJid
  • Filtered out messages with decryption errors via messageStubParameters
  • Added unreadMessages in ChannelStartupService response
  • Updated Typebot controller to lookup instances by name
whatsapp.baileys.service.ts
channel.service.ts
typebot.controller.ts
Hardened media download and S3 upload with validation and fallback
  • Introduced hasValidMediaContent helper to skip empty/stub media
  • Wrapped all S3 upload blocks in media‐validity checks
  • Added fallback using downloadContentFromMessage with mapMediaType on download failures
  • Consolidated common upload logic into validated branches
whatsapp.baileys.service.ts
whatsapp.business.service.ts
evolution.channel.service.ts
Added mapMediaType utility for Baileys media fallback
  • Implemented mapMediaType to translate Baileys content keys to media types
  • Leveraged mapMediaType in downloadContentFromMessage fallback path
whatsapp.baileys.service.ts
Enhanced Chatwoot integration with contact merging logic
  • Added mergeContacts method and invoked it when updateContact returns null
  • Switched to checking previousRemoteJid for LID updates
  • Unified remoteJid resolution across createConversation logic
chatwoot.service.ts
Version bumps and Docker improvements
  • Updated package.json and Dockerfile to v2.3.1
  • Switched to npm ci and included tsup.config.ts in builder
  • Updated swarm YAML image reference
  • Upgraded Jimp and Sharp dependencies
package.json
Dockerfile
evolution_api_v2.yaml

Possibly linked issues


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 @alkimimcitty - I've reviewed your changes - here's some feedback:

  • There’s duplicated media validation and S3 upload code across services; consider extracting it into a shared helper to reduce repetition and improve maintainability.
  • The '@lid' JID normalization logic appears in multiple handlers; centralizing that transformation into a single utility would ensure consistency.
  • The fallback path in getBase64FromMediaMessage retries the download once but lacks a retry limit or clear error propagation—consider adding a defined retry policy and proper error handling.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There’s duplicated media validation and S3 upload code across services; consider extracting it into a shared helper to reduce repetition and improve maintainability.
- The '@lid' JID normalization logic appears in multiple handlers; centralizing that transformation into a single utility would ensure consistency.
- The fallback path in getBase64FromMediaMessage retries the download once but lacks a retry limit or clear error propagation—consider adding a defined retry policy and proper error handling.

## Individual Comments

### Comment 1
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1047` </location>
<code_context>
                   },
                 });

-                if (
-                  openAiDefaultSettings &&
-                  openAiDefaultSettings.openaiCredsId &&
</code_context>

<issue_to_address>
Logging entire received object may expose sensitive data.

Log only necessary identifiers or error details to avoid exposing sensitive information.
</issue_to_address>

### Comment 2
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:3511` </location>
<code_context>
+      let buffer: Buffer;
</code_context>

<issue_to_address>
Fallback download logic does not propagate errors or handle all failure scenarios.

If both download attempts fail, ensure the function returns or throws an error to prevent undefined buffer usage.
</issue_to_address>

### Comment 3
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:570` </location>
<code_context>
   public async createConversation(instance: InstanceDto, body: any) {
-    const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
-    const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
+    const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
+    const remoteJid = body.key.remoteJid;
     const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
     const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
</code_context>

<issue_to_address>
Switching to previousRemoteJid for isLid logic may break compatibility with older message formats.

If previousRemoteJid is missing, this logic could fail. Please ensure previousRemoteJid is always set or add a fallback mechanism.
</issue_to_address>

### Comment 4
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:592` </location>
<code_context>
             phone_number: `+${body.key.senderPn.split('@')[0]}`,
           });
+
+          if (updateContact === null) {
+            const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
+            if (baseContact) {
+              await this.mergeContacts(baseContact.id, contact.id);
+              this.logger.verbose(
+                `Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
+              );
+            }
+          }
</code_context>

<issue_to_address>
mergeContacts is called without checking for race conditions or merge conflicts.

Concurrent merges may cause data inconsistencies. Please implement locking or conflict resolution if concurrent updates are possible.

Suggested implementation:

```typescript
          if (updateContact === null) {
            const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
            if (baseContact) {
              // Acquire lock before merging
              const lockAcquired = await this.acquireContactMergeLock([baseContact.id, contact.id]);
              if (!lockAcquired) {
                this.logger.warn(
                  `Could not acquire lock for merging contacts: (${baseContact.id}) and (${contact.id}). Merge skipped to avoid race condition.`
                );
                return;
              }
              try {
                await this.mergeContacts(baseContact.id, contact.id);
                this.logger.verbose(
                  `Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
                );
              } finally {
                this.releaseContactMergeLock([baseContact.id, contact.id]);
              }
            }
          }

```

```typescript

  // In-memory lock for contact merges
  private contactMergeLocks: Set<number> = new Set();

  private async acquireContactMergeLock(contactIds: number[]): Promise<boolean> {
    // Sort to avoid deadlocks
    contactIds.sort((a, b) => a - b);
    for (const id of contactIds) {
      if (this.contactMergeLocks.has(id)) {
        return false;
      }
    }
    contactIds.forEach(id => this.contactMergeLocks.add(id));
    return true;
  }

  private releaseContactMergeLock(contactIds: number[]) {
    contactIds.forEach(id => this.contactMergeLocks.delete(id));
  }

  private async mergeContacts(baseId: number, mergeId: number) {
    try {
      const contact = await chatwootRequest(this.getClientCwConfig(), {
        method: 'POST',
        url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
        body: {
          base_contact_id: baseId,
          mergee_contact_id: mergeId,

```
</issue_to_address>

### Comment 5
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:2138` </location>
<code_context>
+            w: 320,
+            h: 180,
+          });
+          const processedBuffer = await img.getBuffer(JimpMime.png);

           const fileStream = new Readable();
</code_context>

<issue_to_address>
JimpMime.png may not be defined if JimpMime is not imported or initialized.

Please import and initialize JimpMime before using it here to prevent runtime errors. Also, confirm compatibility with previous usage of Jimp.MIME_PNG.
</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 +1047 to +1056
if (
received?.messageStubParameters?.some?.((param) =>
[
'No matching sessions found for message',
'Bad MAC',
'failed to decrypt message',
'SessionError',
'Invalid PreKey ID',
].some((err) => param?.includes?.(err)),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): Logging entire received object may expose sensitive data.

Log only necessary identifiers or error details to avoid exposing sensitive information.

Comment on lines +3511 to +3520
let buffer: Buffer;

try {
buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message },
'buffer',
{},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
} catch (err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Fallback download logic does not propagate errors or handle all failure scenarios.

If both download attempts fail, ensure the function returns or throws an error to prevent undefined buffer usage.

Comment on lines +570 to +571
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = body.key.remoteJid;
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Switching to previousRemoteJid for isLid logic may break compatibility with older message formats.

If previousRemoteJid is missing, this logic could fail. Please ensure previousRemoteJid is always set or add a fallback mechanism.

const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`,
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): mergeContacts is called without checking for race conditions or merge conflicts.

Concurrent merges may cause data inconsistencies. Please implement locking or conflict resolution if concurrent updates are possible.

Suggested implementation:

          if (updateContact === null) {
            const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
            if (baseContact) {
              // Acquire lock before merging
              const lockAcquired = await this.acquireContactMergeLock([baseContact.id, contact.id]);
              if (!lockAcquired) {
                this.logger.warn(
                  `Could not acquire lock for merging contacts: (${baseContact.id}) and (${contact.id}). Merge skipped to avoid race condition.`
                );
                return;
              }
              try {
                await this.mergeContacts(baseContact.id, contact.id);
                this.logger.verbose(
                  `Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
                );
              } finally {
                this.releaseContactMergeLock([baseContact.id, contact.id]);
              }
            }
          }
  // In-memory lock for contact merges
  private contactMergeLocks: Set<number> = new Set();

  private async acquireContactMergeLock(contactIds: number[]): Promise<boolean> {
    // Sort to avoid deadlocks
    contactIds.sort((a, b) => a - b);
    for (const id of contactIds) {
      if (this.contactMergeLocks.has(id)) {
        return false;
      }
    }
    contactIds.forEach(id => this.contactMergeLocks.add(id));
    return true;
  }

  private releaseContactMergeLock(contactIds: number[]) {
    contactIds.forEach(id => this.contactMergeLocks.delete(id));
  }

  private async mergeContacts(baseId: number, mergeId: number) {
    try {
      const contact = await chatwootRequest(this.getClientCwConfig(), {
        method: 'POST',
        url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
        body: {
          base_contact_id: baseId,
          mergee_contact_id: mergeId,

w: 320,
h: 180,
});
const processedBuffer = await img.getBuffer(JimpMime.png);
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): JimpMime.png may not be defined if JimpMime is not imported or initialized.

Please import and initialize JimpMime before using it here to prevent runtime errors. Also, confirm compatibility with previous usage of Jimp.MIME_PNG.

mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Invert ternary operator to remove negation (invert-ternary)

Suggested change
mimetype = !mimetype ? 'audio/mp4' : mimetype;
mimetype = mimetype ? mimetype : 'audio/mp4';


ExplanationNegated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By inverting the ternary condition and swapping the
expressions we can simplify the code.

mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Invert ternary operator to remove negation (invert-ternary)

Suggested change
mimetype = !mimetype ? 'video/mp4' : mimetype;
mimetype = mimetype ? mimetype : 'video/mp4';


ExplanationNegated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By inverting the ternary condition and swapping the
expressions we can simplify the code.

} else {
mediaType = 'video';
}
const id = message.messages[0][message.messages[0].type].id;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Prefer object destructuring when accessing and using properties. (use-object-destructuring)

Suggested change
const id = message.messages[0][message.messages[0].type].id;
const {id} = message.messages[0][message.messages[0].type];


ExplanationObject destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the Airbnb Javascript Style Guide

Comment on lines +462 to +471
const contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
body: {
base_contact_id: baseId,
mergee_contact_id: mergeId,
},
});

return contact;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
const contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
body: {
base_contact_id: baseId,
mergee_contact_id: mergeId,
},
});
return contact;
return await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
body: {
base_contact_id: baseId,
mergee_contact_id: mergeId,
},
});


ExplanationSomething that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.

const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = body.key.remoteJid;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Prefer object destructuring when accessing and using properties. (use-object-destructuring)

Suggested change
const remoteJid = body.key.remoteJid;
const {remoteJid} = body.key;


ExplanationObject destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the Airbnb Javascript Style Guide

@alkimimcitty alkimimcitty closed this by deleting the head repository Jul 11, 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.

10 participants