Skip to content

Conversation

@VCalazans
Copy link
Contributor

@VCalazans VCalazans commented Jul 29, 2025

Summary by Sourcery

Standardize JID handling and enhance robustness, logging, and session management in the WhatsApp integration; automate message status updates; refine caching for WhatsApp contacts; default speech-to-text behavior and auto-select new OpenAI credentials; adjust Docker build flags; update messageId logic in EvoAI; and introduce database migrations for speechToText defaults

New Features:

  • Introduce normalizeJid utility and clearCorruptedSessionData functions in WhatsApp service to standardize identifiers and purge corrupted sessions
  • Automatically force-update PENDING WhatsApp messages status after a 30-second timeout
  • Automatically set newly created OpenAI credentials as the default in settings

Enhancements:

  • Add detailed logging for connection states, message sending, and error conditions across the Baileys service
  • Normalize JIDs throughout message processing for cache keys, database lookups, and status updates
  • Refactor message update flow to avoid duplicate processing and ensure consistent status handling
  • Extend onWhatsappCache to return full JIDs with domains and prioritize Brazilian number formats
  • Enable speechToText by default in the OpenAI controller with override support
  • Wrap conversation sendMessage calls in try/catch with logging
  • Change EvoAI service to derive messageId from phone number when available

Build:

  • Update Dockerfile npm install to use --force --legacy-peer-deps

Chores:

  • Add Prisma migrations to set default speechToText to true for existing OpenaiSetting records and database schemas

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jul 29, 2025

Reviewer's Guide

This PR merges multiple solutions by introducing JID normalization and session cleanup routines, overhauling message key generation with normalized JIDs, enhancing caching and status update logic in the Baileys WhatsApp integration, improving logging and error handling, refining on-WhatsApp number verification, updating default speechToText behavior and auto-setting credentials in the OpenAI controller with accompanying database migrations, adjusting dependency installation in Docker, and tuning messageId derivation in the EvoAI service.

ER diagram for OpenaiSetting speechToText default change

erDiagram
    OpenaiSetting {
        BOOLEAN speechToText "DEFAULT true"
    }
    OpenaiSetting ||--o| OpenaiCreds : uses
    OpenaiSetting ||--o| Instance : belongs_to
Loading

Class diagram for JID normalization and session cleanup utilities

classDiagram
    class BaileysStartupService {
        +normalizeJid(jid: string): string
        +clearCorruptedSessionData(instanceId: string, baileysCache: CacheService): Promise<void>
    }
    class CacheService
    class CacheEngine
    class configService
    BaileysStartupService --> CacheService
    CacheService --> CacheEngine
    CacheEngine --> configService
Loading

Class diagram for OpenaiController credential and speechToText logic

classDiagram
    class OpenaiController {
        +setCredentialAsDefault(instanceId: string, credentialId: string): Promise<void>
        +createCreds(instance: InstanceDto, data: any): Promise<any>
    }
    class credsRepository
    class settingsRepository
    OpenaiController --> credsRepository
    OpenaiController --> settingsRepository
Loading

Class diagram for onWhatsappCache number format logic

classDiagram
    class onWhatsappCache {
        +getAvailableNumbers(remoteJid: string): string[]
        +saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]): Promise<void>
    }
    class ISaveOnWhatsappCacheParams {
        +remoteJid: string
        +lid: string | null
    }
    onWhatsappCache --> ISaveOnWhatsappCacheParams
Loading

Class diagram for EvoaiService messageId logic update

classDiagram
    class EvoaiService {
        +sendMessage(msg: any, remoteJid: string): Promise<any>
    }
    EvoaiService : - messageId = remoteJid.split('@')[0] || uuidv4()
Loading

File-Level Changes

Change Details Files
Added JID normalization and corrupted session cleanup
  • Implement normalizeJid to convert LIDs and strip suffixes to standard JID
  • Create clearCorruptedSessionData to purge instance-specific cache patterns
  • Invoke session cleanup before establishing Baileys connection
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Refactored messageKey construction, caching and status updates
  • Include normalizedJid in all cache keys and DB lookups
  • Schedule forced SERVER_ACK update for PENDING messages after timeout
  • Use cache to dedupe read receipt handling in message.update
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Enhanced logging and error handling in connection and send operations
  • Log connection state changes and warning on disconnect errors
  • Wrap sendMessage calls in try/catch to log attempts, successes, and failures
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Improved on-WhatsApp number verification and cache logic
  • Filter Brazilian numbers to prioritize formats with or without the '9' digit
  • Skip verification for cached numbers unless format correction is needed
  • Adjust onWhatsappCache to return full JID array with prioritized order
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
src/utils/onWhatsappCache.ts
Updated OpenAI controller defaults and auto-default credential setup
  • Enable speechToText by default and remove API key uniqueness enforcement
  • Add setCredentialAsDefault to automatically link new creds in settings
  • Modify controller to default speechToText=true when undefined
src/api/integrations/chatbot/openai/controllers/openai.controller.ts
Add Prisma migrations to enforce default speechToText behavior
  • Alter OpenaiSetting.speechToText default to true
  • Update existing records where speechToText is null or false
prisma/mysql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql
prisma/postgresql-migrations/20250709000000_change_speech_to_text_default_to_true/migration.sql
Adjust Dockerfile and EvoAI messageId derivation
  • Use --force --legacy-peer-deps for npm install in Dockerfile
  • Generate messageId in EvoaiService from remoteJid phone number part
Dockerfile
src/api/integrations/chatbot/evoai/services/evoai.service.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 @VCalazans - 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/channel/whatsapp/whatsapp.baileys.service.ts:178` </location>
<code_context>
+}
+
+// Function to clear corrupted session data
+async function clearCorruptedSessionData(instanceId: string, baileysCache: CacheService) {
+  try {
+    // Clear all baileys cache for this instance
</code_context>

<issue_to_address>
The cache clearing patterns in clearCorruptedSessionData may be overly broad.

The current cache deletion patterns may match unintended keys, risking accidental data loss. Please refine them to target only the necessary session data.

Suggested implementation:

```typescript
    // Clear only session-related cache keys for this instance
    const patterns = [
      `${instanceId}:session*`,
      `${instanceId}:prekey*`,
      `${instanceId}:app-state-sync-key*`,
      `${instanceId}:signal-identities*`
    ];


```

```typescript
    for (const pattern of patterns) {
      await baileysCache.deleteByPattern(pattern);
    }

```
</issue_to_address>

### Comment 2
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:426` </location>
<code_context>
         statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200,
       };
+      
+      this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
+      if (lastDisconnect?.error) {
+        this.logger.warn(`Connection error: ${JSON.stringify(lastDisconnect.error)}`);
+      }
</code_context>

<issue_to_address>
Logging sensitive error details may expose internal information.

Sanitize or redact sensitive fields from lastDisconnect.error before logging, particularly in production.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
      this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
      if (lastDisconnect?.error) {
        this.logger.warn(`Connection error: ${JSON.stringify(lastDisconnect.error)}`);
      }
=======
      this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
      if (lastDisconnect?.error) {
        // Sanitize error object before logging
        const error = lastDisconnect.error;
        const sanitizedError = { ...error };

        // Redact sensitive fields if present
        if ('stack' in sanitizedError) sanitizedError.stack = '[REDACTED]';
        if ('message' in sanitizedError && process.env.NODE_ENV === 'production') sanitizedError.message = '[REDACTED]';
        if ('output' in sanitizedError && sanitizedError.output?.payload) sanitizedError.output.payload = '[REDACTED]';

        this.logger.warn(`Connection error: ${JSON.stringify(sanitizedError)}`);
      }
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:709` </location>
<code_context>
     this.endSession = false;

+    // Clear any corrupted session data before connecting
+    await clearCorruptedSessionData(this.instanceId, this.baileysCache);
+
     this.client = makeWASocket(socketConfig);
</code_context>

<issue_to_address>
Clearing session data on every connect may impact performance.

If clearing session data isn't always required, consider making this operation conditional or rate-limited to avoid unnecessary latency, especially under high load.

Suggested implementation:

```typescript
    // Rate-limit clearing corrupted session data to avoid unnecessary latency
    const SESSION_CLEAR_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
    if (!this.lastSessionClear || Date.now() - this.lastSessionClear > SESSION_CLEAR_INTERVAL_MS) {
      await clearCorruptedSessionData(this.instanceId, this.baileysCache);
      this.lastSessionClear = Date.now();
    }

    this.client = makeWASocket(socketConfig);

```

You must ensure that `this.lastSessionClear` is initialized in the class constructor, e.g.:
```ts
this.lastSessionClear = 0;
```
If you want a different condition (e.g., a flag or a more advanced rate limiter), adjust the logic accordingly.
</issue_to_address>

### Comment 4
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4290` </location>
<code_context>
+      this.logger.warn(`Message sent with PENDING status - ID: ${message.key.id}, Instance: ${this.instance.id}, Status: ${message.status}, RemoteJid: ${message.key.remoteJid}`);
+    }
+
+    if (!messageRaw.status) {
+      if (message.key.fromMe === false) {
+        messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
+      } else {
+        messageRaw.status = status[2]; // SERVER_ACK for sent messages without status
+      }
     }

</code_context>

<issue_to_address>
Defaulting sent message status to SERVER_ACK may mask delivery issues.

Review whether SERVER_ACK is the appropriate default for all sent messages, or if stricter checks are needed to avoid hiding delivery problems.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    if (!messageRaw.status) {
      if (message.key.fromMe === false) {
        messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
      } else {
        messageRaw.status = status[2]; // SERVER_ACK for sent messages without status
      }
     }
=======
    if (!messageRaw.status) {
      if (message.key.fromMe === false) {
        messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
      } else {
        // Only set SERVER_ACK if we have evidence of server acknowledgement
        if (message.status && message.status >= status[2]) {
          messageRaw.status = status[2]; // SERVER_ACK for sent messages with server ack
        } else {
          // Otherwise, set to PENDING and log a warning
          messageRaw.status = status[1]; // PENDING
          this.logger.warn(
            `Sent message without explicit server ack - defaulting to PENDING. ID: ${message.key.id}, Instance: ${this.instance.id}, Status: ${message.status}, RemoteJid: ${message.key.remoteJid}`
          );
        }
      }
    }
>>>>>>> REPLACE

</suggested_fix>

### Comment 5
<location> `src/api/integrations/chatbot/openai/controllers/openai.controller.ts:239` </location>
<code_context>
     if (!data.name) throw new BadRequestException('Name is required');

     // Check if API key already exists
-    const existingApiKey = await this.credsRepository.findFirst({
-      where: {
-        apiKey: data.apiKey,
-      },
-    });
+    // const existingApiKey = await this.credsRepository.findFirst({
+    //   where: {
+    //     apiKey: data.apiKey,
+    //   },
+    // });

-    if (existingApiKey) {
-      throw new BadRequestException('This API key is already registered. Please use a different API key.');
-    }
</code_context>

<issue_to_address>
Removing API key uniqueness check may allow duplicate credentials.

If allowing duplicate API keys is intentional, confirm that downstream systems can handle this and that it won't cause security or billing problems.
</issue_to_address>

### Comment 6
<location> `src/api/integrations/chatbot/openai/controllers/openai.controller.ts:303` </location>
<code_context>
     return creds;
   }

+  private async setCredentialAsDefault(instanceId: string, credentialId: string) {
+    try {
+      // Check if settings already exist for this instance
</code_context>

<issue_to_address>
setCredentialAsDefault swallows errors, which may hide failures.

Consider propagating or reporting errors instead of only logging them, to ensure failures in setting the default credential are detected and handled appropriately.
</issue_to_address>

### Comment 7
<location> `src/api/integrations/chatbot/evoai/services/evoai.service.ts:73` </location>
<code_context>
       }

       const callId = `req-${uuidv4().substring(0, 8)}`;
-      const messageId = msg?.key?.id || uuidv4();
+      const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId

       // Prepare message parts
</code_context>

<issue_to_address>
Using the phone number as messageId may cause collisions.

This approach can lead to duplicate message IDs for the same recipient. To avoid tracking or deduplication issues, use a unique identifier such as a timestamp or random suffix.
</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.

}

// Function to clear corrupted session data
async function clearCorruptedSessionData(instanceId: string, baileysCache: CacheService) {
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): The cache clearing patterns in clearCorruptedSessionData may be overly broad.

The current cache deletion patterns may match unintended keys, risking accidental data loss. Please refine them to target only the necessary session data.

Suggested implementation:

    // Clear only session-related cache keys for this instance
    const patterns = [
      `${instanceId}:session*`,
      `${instanceId}:prekey*`,
      `${instanceId}:app-state-sync-key*`,
      `${instanceId}:signal-identities*`
    ];
    for (const pattern of patterns) {
      await baileysCache.deleteByPattern(pattern);
    }

Comment on lines +426 to +429
this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
if (lastDisconnect?.error) {
this.logger.warn(`Connection error: ${JSON.stringify(lastDisconnect.error)}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 suggestion (security): Logging sensitive error details may expose internal information.

Sanitize or redact sensitive fields from lastDisconnect.error before logging, particularly in production.

Suggested change
this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
if (lastDisconnect?.error) {
this.logger.warn(`Connection error: ${JSON.stringify(lastDisconnect.error)}`);
}
this.logger.log(`Connection state changed to: ${connection}, instance: ${this.instance.id}`);
if (lastDisconnect?.error) {
// Sanitize error object before logging
const error = lastDisconnect.error;
const sanitizedError = { ...error };
// Redact sensitive fields if present
if ('stack' in sanitizedError) sanitizedError.stack = '[REDACTED]';
if ('message' in sanitizedError && process.env.NODE_ENV === 'production') sanitizedError.message = '[REDACTED]';
if ('output' in sanitizedError && sanitizedError.output?.payload) sanitizedError.output.payload = '[REDACTED]';
this.logger.warn(`Connection error: ${JSON.stringify(sanitizedError)}`);
}

this.endSession = false;

// Clear any corrupted session data before connecting
await clearCorruptedSessionData(this.instanceId, this.baileysCache);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (performance): Clearing session data on every connect may impact performance.

If clearing session data isn't always required, consider making this operation conditional or rate-limited to avoid unnecessary latency, especially under high load.

Suggested implementation:

    // Rate-limit clearing corrupted session data to avoid unnecessary latency
    const SESSION_CLEAR_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
    if (!this.lastSessionClear || Date.now() - this.lastSessionClear > SESSION_CLEAR_INTERVAL_MS) {
      await clearCorruptedSessionData(this.instanceId, this.baileysCache);
      this.lastSessionClear = Date.now();
    }

    this.client = makeWASocket(socketConfig);

You must ensure that this.lastSessionClear is initialized in the class constructor, e.g.:

this.lastSessionClear = 0;

If you want a different condition (e.g., a flag or a more advanced rate limiter), adjust the logic accordingly.

Comment on lines +4290 to 4296
if (!messageRaw.status) {
if (message.key.fromMe === false) {
messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
} else {
messageRaw.status = status[2]; // SERVER_ACK for sent messages without status
}
}
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): Defaulting sent message status to SERVER_ACK may mask delivery issues.

Review whether SERVER_ACK is the appropriate default for all sent messages, or if stricter checks are needed to avoid hiding delivery problems.

Suggested change
if (!messageRaw.status) {
if (message.key.fromMe === false) {
messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
} else {
messageRaw.status = status[2]; // SERVER_ACK for sent messages without status
}
}
if (!messageRaw.status) {
if (message.key.fromMe === false) {
messageRaw.status = status[3]; // DELIVERED MESSAGE for received messages
} else {
// Only set SERVER_ACK if we have evidence of server acknowledgement
if (message.status && message.status >= status[2]) {
messageRaw.status = status[2]; // SERVER_ACK for sent messages with server ack
} else {
// Otherwise, set to PENDING and log a warning
messageRaw.status = status[1]; // PENDING
this.logger.warn(
`Sent message without explicit server ack - defaulting to PENDING. ID: ${message.key.id}, Instance: ${this.instance.id}, Status: ${message.status}, RemoteJid: ${message.key.remoteJid}`
);
}
}
}

Comment on lines -239 to -245
const existingApiKey = await this.credsRepository.findFirst({
where: {
apiKey: data.apiKey,
},
});
// const existingApiKey = await this.credsRepository.findFirst({
// where: {
// apiKey: data.apiKey,
// },
// });

if (existingApiKey) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 question (security): Removing API key uniqueness check may allow duplicate credentials.

If allowing duplicate API keys is intentional, confirm that downstream systems can handle this and that it won't cause security or billing problems.

return creds;
}

private async setCredentialAsDefault(instanceId: string, credentialId: string) {
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): setCredentialAsDefault swallows errors, which may hide failures.

Consider propagating or reporting errors instead of only logging them, to ensure failures in setting the default credential are detected and handled appropriately.

}

const callId = `req-${uuidv4().substring(0, 8)}`;
const messageId = msg?.key?.id || uuidv4();
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): Using the phone number as messageId may cause collisions.

This approach can lead to duplicate message IDs for the same recipient. To avoid tracking or deduplication issues, use a unique identifier such as a timestamp or random suffix.

@VCalazans VCalazans closed this Jul 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.

2 participants