Skip to content

Conversation

@KokeroO
Copy link
Contributor

@KokeroO KokeroO commented Oct 30, 2025

Summary by Sourcery

Improve integration between Chatwoot and Baileys services by fixing identifier mapping, standardizing phone formatting, enriching logs, refactoring cache and startup logic, extending DTOs with connectionStatus/ownerJid, and bumping the package version

New Features:

  • Automatically format WhatsApp phone numbers internationally using libphonenumber-js
  • Propagate connectionStatus and ownerJid fields through InstanceDto for unified controller/service handling

Bug Fixes:

  • Fix contact identifier update in Chatwoot integration to use actual phone number instead of remoteJid

Enhancements:

  • Enhance verbose logging across Chatwoot and Baileys services with contextual sender and conversation details
  • Refactor onWhatsappCache to use upsert for simpler record management
  • Reduce conversation cache TTL from 8 hours to 30 minutes to avoid stale data
  • Clean up redundant code paths and commented-out blocks in Baileys and Chatwoot services
  • Make makeProxyAgentUndici syntax consistent and add proper semicolons
  • Convert initWA to async with error handling during application bootstrap

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 30, 2025

Reviewer's Guide

This PR refines the Chatwoot-Baileys integration by enhancing logging and caching in ChatwootService, standardizing DTOs and controller logic, improving instance lifecycle management, consolidating phone formatting and message handling, cleaning up utilities, and updating versioning and dependencies.

Sequence diagram for improved instance restart logic in InstanceController

sequenceDiagram
  participant User
  participant InstanceController
  participant Instance
  participant WAMonitoringService

  User->>InstanceController: Request to restart instance
  InstanceController->>Instance: Check connectionStatus
  alt Instance has restart() method
    InstanceController->>Instance: Call restart()
    InstanceController->>Instance: Wait for reconnection
    InstanceController-->>User: Return instance status
  else Baileys fallback
    InstanceController->>Instance: Close ws and end client
    InstanceController->>WAMonitoringService: connectToWhatsapp()
    WAMonitoringService-->>InstanceController: Instance reconnected
    InstanceController-->>User: Return instance status
  end
Loading

Sequence diagram for improved instance deletion timeout management in WAMonitoringService

sequenceDiagram
  participant WAMonitoringService
  participant Instance
  participant EventEmitter

  WAMonitoringService->>WAMonitoringService: delInstanceTime(instance)
  alt Timeout exists
    WAMonitoringService->>WAMonitoringService: clear previous timeout
  end
  WAMonitoringService->>WAMonitoringService: set new timeout
  WAMonitoringService->>Instance: Check connectionStatus
  alt connectionStatus == 'connecting' and integration == Baileys
    Instance->>Instance: logout()
    Instance->>Instance: ws.close()
    Instance->>Instance: end()
    WAMonitoringService->>EventEmitter: emit remove.instance
  else
    WAMonitoringService->>EventEmitter: emit remove.instance
  end
  WAMonitoringService->>WAMonitoringService: delete timeout reference
Loading

Class diagram for updated InstanceDto and related types

classDiagram
  class InstanceDto {
    +instanceName: string
    +instanceId: string
    +connectionStatus: string
    +token: string
    +status: string
    +ownerJid: string
    +profileName: string
    +profilePicUrl: string
    ...
  }
  class IntegrationDto {
    ...
  }
  InstanceDto --|> IntegrationDto

  class wa.Instance {
    +instanceName: string
    +instanceId: string
    +connectionStatus: string
    +ownerJid: string
    ...
  }
  InstanceDto <.. wa.Instance : used for data transfer
Loading

Class diagram for updated ChatwootController and ChatwootService relationship

classDiagram
  class ChatwootController {
    -chatwootService: ChatwootService
    -configService: ConfigService
    +createChatwoot(instance: InstanceDto, data: ChatwootDto)
    +receiveWebhook(instance: InstanceDto, data: any)
  }
  class ChatwootService {
    ...
  }
  ChatwootController --> ChatwootService : uses
Loading

Class diagram for WAMonitoringService instance timeout management

classDiagram
  class WAMonitoringService {
    +waInstances: Record<string, any>
    -delInstanceTimeouts: Record<string, NodeJS.Timeout>
    +delInstanceTime(instance: string)
    +clearDelInstanceTime(instance: string)
    ...
  }
Loading

Class diagram for updated phone number formatting in ChatwootService

classDiagram
  class ChatwootService {
    +parsePhoneNumberFromString(phone: string): PhoneNumber
    ...
  }
  class PhoneNumber {
    +formatInternational(): string
    ...
  }
  ChatwootService --> PhoneNumber : uses
Loading

File-Level Changes

Change Details Files
Unified ChatwootService enhancements: logging, cache, formatting, and message handling
  • switched identifier updates to use phoneNumber, reduced conversation cache TTL to 30 minutes, and removed redundant lock re-check
  • replaced JSON.stringify logs with field-based messages for conversations, group metadata, contacts, and participant events
  • migrated manual phone number regex to libphonenumber-js parsePhoneNumberFromString.formatInternational
  • unified edited message extraction across types, trimmed content, and early-ignore empty edits
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
package.json
InstanceController: standardized DTO usage and restart/logout logic
  • introduced InstanceDto with connectionStatus mapping and used it for proxy creation and settings
  • added async restart support for modern instances and fallback to connectToWhatsapp for Baileys
  • awaited logoutInstance call to ensure proper disconnect
src/api/controllers/instance.controller.ts
WAMonitoringService: per-instance timeouts and auto-connect logic
  • track delInstanceTimeouts per instance, clear previous timeouts, and clean up after execution
  • auto-connect instances only when status is open or connecting, skip otherwise
  • propagate connectionStatus and ownerJid in instanceData during load and update
  • provide clearDelInstanceTime to cancel scheduled removals on logout or removal
src/api/services/monitor.service.ts
Simplified onWhatsappCache operations with Prisma upsert
  • replaced separate update/create blocks with a single prismaRepository.isOnWhatsapp.upsert call
src/utils/onWhatsappCache.ts
Cleaned up Baileys service startup logic
  • removed commented-out Baileys version fallback code
  • eliminated manual duplicate message caching block
  • refined log message to distinguish update events
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Bootstrap and controller instantiation adjustments
  • converted initWA to async/await with error handling in main.ts
  • removed prismaRepository injection from ChatwootController and cleaned server.module.ts instantiation
  • adjusted ChatwootController to use injected ChatwootService instead of recreating
src/main.ts
src/api/server.module.ts
src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts
Config, DTO, and type enhancements for connectionState and ownerJid
  • added connectionStatus to InstanceDto and wa.types
  • added ownerJid propagation in ChannelStartupService
src/api/dto/instance.dto.ts
src/api/types/wa.types.ts
src/api/services/channel.service.ts
Application version bump and dependency updates
  • bumped package.json version to 2.3.7 and Docker image tag to v2.3.7
  • updated .github ISSUE_TEMPLATE to reflect new API version
  • added libphonenumber-js dependency
package.json
Docker/swarm/evolution_api_v2.yaml
.github/ISSUE_TEMPLATE/bug_report.yml
Utility code cleanup in makeProxyAgent
  • added missing semicolons, standardized variable declarations, and removed stray import newline
src/utils/makeProxyAgent.ts

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 and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:615` </location>
<code_context>
         const conversationId = (await this.cache.get(cacheKey)) as number;
         this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
-        let conversationExists: conversation | boolean;
+        let conversationExists: any;
         try {
           conversationExists = await client.conversations.get({
</code_context>

<issue_to_address>
**suggestion:** Switching conversationExists type to any reduces type safety.

Consider using a union type or a descriptive type alias if the value can be multiple types, rather than 'any', to maintain type safety and clarity.

Suggested implementation:

```typescript
        type ConversationOrFalse = conversation | false;
        let conversationExists: ConversationOrFalse;

```

- If the `conversation` type is not already imported or defined, ensure it is imported from the correct module.
- If you prefer, you can define the type alias at the top of the file or near other type definitions for better visibility.
</issue_to_address>

### Comment 2
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:622` </location>
<code_context>
           });
-          this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
+          this.logger.verbose(
+            `Conversation exists: ID: ${conversationExists.id} - Name: ${conversationExists.meta.sender.name} - Identifier: ${conversationExists.meta.sender.identifier}`,
+          );
         } catch (error) {
</code_context>

<issue_to_address>
**issue:** Logging assumes meta.sender properties always exist.

If conversationExists or meta.sender may be undefined, use optional chaining or fallback values to prevent runtime errors in the log statement.
</issue_to_address>

### Comment 3
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:675` </location>
<code_context>
           this.logger.verbose(`Processing group conversation`);
           const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
-          this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
+          this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);

           const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
</code_context>

<issue_to_address>
**nitpick (typo):** Potential typo: group.JID may be undefined.

Verify whether the correct property is 'id' instead of 'JID' to avoid logging undefined values.

```suggestion
          this.logger.verbose(`Group metadata: ID:${group.id} - Subject:${group?.subject || group?.Name}`);
```
</issue_to_address>

### Comment 4
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:803-811` </location>
<code_context>
       if (await this.cache.has(cacheKey)) {
         const conversationId = (await this.cache.get(cacheKey)) as number;
         this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
</code_context>

<issue_to_address>
**issue (bug_risk):** Removing triple-check after lock may introduce race conditions.

Please confirm how duplicate conversation creation is prevented, or specify where locking is enforced if handled elsewhere.
</issue_to_address>

### Comment 5
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:1386` </location>
<code_context>
-          body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:' &&
-          body?.conversation?.messages[0]?.id === body?.id
-        ) {
+        if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
           return { message: 'bot' };
         }
</code_context>

<issue_to_address>
**issue (bug_risk):** Relaxing outgoing message check may allow false positives.

Please verify that removing the message ID comparison does not cause the bot to respond to messages it shouldn't.
</issue_to_address>

### Comment 6
<location> `src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:2025` </location>
<code_context>
-            }
+                ? body.key.participantAlt.split('@')[0].split(':')[0]
+                : body.key.participant.split('@')[0].split(':')[0];
+            const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();

             let content: string;
</code_context>

<issue_to_address>
**issue:** Using libphonenumber-js for formatting improves consistency but may throw.

Handle cases where parsePhoneNumberFromString returns undefined or throws to prevent runtime errors.
</issue_to_address>

### Comment 7
<location> `src/utils/onWhatsappCache.ts:120` </location>
<code_context>
-          },
-        });
-      }
+      await prismaRepository.isOnWhatsapp.upsert({
+        where: { remoteJid: remoteJid },
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Switching to upsert simplifies cache logic and improves atomicity.

Please verify that the database enforces a unique constraint on remoteJid to maintain data integrity.

Suggested implementation:

```typescript
/**
 * NOTE: Ensure that the database enforces a unique constraint on the `remoteJid` field
 * in the `isOnWhatsapp` table to guarantee upsert atomicity and data integrity.
 * For Prisma, add `@unique` to the `remoteJid` field in your schema.prisma file:
 *
 * model isOnWhatsapp {
 *   id         Int    @id @default(autoincrement())
 *   remoteJid  String @unique
 *   // ... other fields ...
 * }
 */

```

You must also update your Prisma schema (usually in `prisma/schema.prisma`) to add `@unique` to the `remoteJid` field in the `isOnWhatsapp` model, then run `npx prisma migrate dev` to apply the migration.
</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.

const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
let conversationExists: conversation | boolean;
let conversationExists: any;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Switching conversationExists type to any reduces type safety.

Consider using a union type or a descriptive type alias if the value can be multiple types, rather than 'any', to maintain type safety and clarity.

Suggested implementation:

        type ConversationOrFalse = conversation | false;
        let conversationExists: ConversationOrFalse;
  • If the conversation type is not already imported or defined, ensure it is imported from the correct module.
  • If you prefer, you can define the type alias at the top of the file or near other type definitions for better visibility.

this.logger.verbose(`Processing group conversation`);
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick (typo): Potential typo: group.JID may be undefined.

Verify whether the correct property is 'id' instead of 'JID' to avoid logging undefined values.

Suggested change
this.logger.verbose(`Group metadata: JID:${group.JID} - Subject:${group?.subject || group?.Name}`);
this.logger.verbose(`Group metadata: ID:${group.id} - Subject:${group?.subject || group?.Name}`);

}
? body.key.participantAlt.split('@')[0].split(':')[0]
: body.key.participant.split('@')[0].split(':')[0];
const formattedPhoneNumber = parsePhoneNumberFromString(`+${rawPhoneNumber}`).formatInternational();
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Using libphonenumber-js for formatting improves consistency but may throw.

Handle cases where parsePhoneNumberFromString returns undefined or throws to prevent runtime errors.

},
});
}
await prismaRepository.isOnWhatsapp.upsert({
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): Switching to upsert simplifies cache logic and improves atomicity.

Please verify that the database enforces a unique constraint on remoteJid to maintain data integrity.

Suggested implementation:

/**
 * NOTE: Ensure that the database enforces a unique constraint on the `remoteJid` field
 * in the `isOnWhatsapp` table to guarantee upsert atomicity and data integrity.
 * For Prisma, add `@unique` to the `remoteJid` field in your schema.prisma file:
 *
 * model isOnWhatsapp {
 *   id         Int    @id @default(autoincrement())
 *   remoteJid  String @unique
 *   // ... other fields ...
 * }
 */

You must also update your Prisma schema (usually in prisma/schema.prisma) to add @unique to the remoteJid field in the isOnWhatsapp model, then run npx prisma migrate dev to apply the migration.

} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
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