Skip to content

Conversation

@FaelN1
Copy link
Contributor

@FaelN1 FaelN1 commented Dec 29, 2025

📋 Descrição

Este PR introduz um novo endpoint para salvar contatos e implementa o handler correspondente na integração Baileys.

Principais alterações:

  • Adicionado endpoint save (salvar) no ContactController.
  • Implementada lógica de salvamento de contatos no WhatsAppBaileysService.
  • Atualizado ContactRouter para incluir a nova rota.
  • Adicionado schema de validação em contact.schema.ts.
  • Registrado o novo roteador no ServerModule e IndexRouter.

🔗 Issue Relacionada

None

🧪 Tipo de Alteração

  • 🐛 Correção de bug (alteração sem quebra que corrige um problema)
  • ✨ Nova funcionalidade (alteração sem quebra que adiciona funcionalidade)
  • 💥 Alteração de quebra (correção ou funcionalidade que faria a funcionalidade existente não funcionar como esperado)
  • 📚 Atualização de documentação
  • 🔧 Refatoração (sem alterações funcionais)
  • ⚡ Melhoria de performance
  • 🧹 Limpeza de código
  • 🔒 Correção de segurança

🧪 Testes

  • Testes manuais concluídos
  • Funcionalidade verificada em ambiente de desenvolvimento
  • Nenhuma alteração de quebra introduzida
  • Testado com diferentes tipos de conexão (se aplicável)

📸 Screenshots (se aplicável)

None

✅ Checklist

  • Meu código segue as diretrizes de estilo do projeto
  • Realizei uma auto-revisão do meu código
  • Comentei meu código, particularmente em áreas difíceis de entender
  • Fiz as alterações correspondentes na documentação
  • Minhas alterações não geram novos avisos
  • Testei manualmente minhas alterações exaustivamente
  • Verifiquei se as alterações funcionam com diferentes cenários
  • Quaisquer alterações dependentes foram mescladas e publicadas

📝 Notas Adicionais

A implementação foca no provedor Baileys por enquanto.

Summary by Sourcery

Add API support for saving WhatsApp contacts through the Baileys integration and expose it via a new contact route.

New Features:

  • Introduce a ContactController with a save endpoint that persists contacts via the WhatsApp Baileys service.
  • Add BaileysStartupService support for saving contacts to a WhatsApp instance using contact details provided by the client.
  • Expose a new /contact/save HTTP route with validation for saving contacts, including a JSON schema and DTO for request payloads.

Copilot AI review requested due to automatic review settings December 29, 2025 13:08
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 29, 2025

Reviewer's Guide

Adds a new save contact HTTP endpoint wired through a ContactRouter and ContactController to the Baileys WhatsApp integration, including DTO/schema validation and Baileys-level implementation to persist contacts via chatModify, and registers the router/controller in the server module and index router.

Sequence diagram for the new save contact endpoint flow

sequenceDiagram
  actor ApiClient
  participant ExpressApp
  participant IndexRouter
  participant ContactRouter
  participant RouterBroker
  participant ContactController
  participant WAMonitoringService
  participant BaileysStartupService
  participant BaileysClient

  ApiClient->>ExpressApp: POST /contact/save { number, name, saveOnDevice }
  ExpressApp->>IndexRouter: route /contact/save
  IndexRouter->>ContactRouter: delegate to /contact
  ContactRouter->>ContactRouter: validate using saveContactSchema
  ContactRouter->>RouterBroker: dataValidate(SaveContactDto)
  RouterBroker-->>ContactRouter: validated SaveContactDto instance
  ContactRouter->>ContactController: saveContact(instanceDto, saveContactDto)
  ContactController->>WAMonitoringService: waInstances[instanceName]
  WAMonitoringService-->>ContactController: BaileysStartupService instance
  ContactController->>BaileysStartupService: saveContact(saveContactDto)
  BaileysStartupService->>BaileysClient: chatModify(contact data, jid)
  BaileysClient-->>BaileysStartupService: result
  BaileysStartupService-->>ContactController: { saved, number, name }
  ContactController-->>ContactRouter: response payload
  ContactRouter-->>ExpressApp: HttpStatus.OK with JSON
  ExpressApp-->>ApiClient: 200 { saved, number, name }
Loading

Updated class diagram for contact save flow types

classDiagram
  class SaveContactDto {
    +string number
    +string name
    +boolean saveOnDevice
  }

  class InstanceDto {
    +string instanceName
  }

  class ContactController {
    -WAMonitoringService waMonitor
    +ContactController(waMonitor)
    +saveContact(instanceDto, data)
  }

  class WAMonitoringService {
    +Map waInstances
  }

  class BaileysStartupService {
    +client
    +saveContact(data)
  }

  class ContactRouter {
    +Router router
    +ContactRouter(guards)
  }

  class RouterBroker {
    +dataValidate(options)
  }

  class saveContactSchema {
  }

  ContactRouter --|> RouterBroker
  ContactRouter --> ContactController : uses
  ContactController --> WAMonitoringService : depends on
  WAMonitoringService --> BaileysStartupService : waInstances values
  BaileysStartupService --> SaveContactDto : parameter
  ContactRouter --> SaveContactDto : validation target
  ContactRouter --> saveContactSchema : validates with
Loading

File-Level Changes

Change Details Files
Expose a new /contact/save HTTP endpoint with validation and controller wiring.
  • Create ContactRouter that defines POST /contact/save using RouterBroker, applying guards and JSON schema validation via dataValidate before delegating to the controller
  • Handle validation errors by returning HTTP 400 and success responses with HTTP 200
  • Register ContactRouter in the main index router under the /contact path
src/api/routes/contact.router.ts
src/api/routes/index.router.ts
Introduce a ContactController that delegates contact saving to the appropriate WhatsApp instance via WAMonitoringService.
  • Create ContactController with a saveContact method that receives InstanceDto and SaveContactDto
  • Use instanceName from InstanceDto to select the correct waMonitor.waInstances entry and call its saveContact method
  • Register a singleton contactController in server.module wired with WAMonitoringService
src/api/controllers/contact.controller.ts
src/api/server.module.ts
Add a SaveContactDto and JSON schema for validating save-contact requests.
  • Define SaveContactDto with number, name, and optional saveOnDevice fields
  • Create saveContactSchema JSONSchema7 with required number and name fields and default true for saveOnDevice
src/api/dto/contact.dto.ts
src/validate/contact.schema.ts
Implement Baileys-level contact persistence logic used by the controller.
  • Import SaveContactDto into BaileysStartupService
  • Add saveContact method that builds a JID from the provided number, calls client.chatModify with a contact payload (fullName, firstName, saveOnPrimaryAddressbook) and returns a success object
  • Wrap chatModify in try/catch and throw InternalServerErrorException with structured error payload on failure
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Possibly linked issues

  • #: A issue solicita rota para criar contatos e o PR adiciona o endpoint /contact/save com toda a lógica necessária.

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 - I've found 4 issues, and left some high level feedback:

  • In ContactRouter, this.router is used inside the constructor before the router field is initialized (field initializers run after super() and before the constructor body, in declaration order), so the router property should be declared/initialized before the constructor or initialized inside the constructor to avoid undefined at runtime.
  • The ContactController.saveContact directly calls this.waMonitor.waInstances[instanceName].saveContact, which assumes every waInstance implements saveContact; if other providers are present, consider narrowing the type or guarding for non-Baileys instances to avoid runtime errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ContactRouter`, `this.router` is used inside the constructor before the `router` field is initialized (field initializers run after `super()` and before the constructor body, in declaration order), so the `router` property should be declared/initialized before the constructor or initialized inside the constructor to avoid `undefined` at runtime.
- The `ContactController.saveContact` directly calls `this.waMonitor.waInstances[instanceName].saveContact`, which assumes every waInstance implements `saveContact`; if other providers are present, consider narrowing the type or guarding for non-Baileys instances to avoid runtime errors.

## Individual Comments

### Comment 1
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:3772-3781` </location>
<code_context>
     }
   }
+  
+  public async saveContact(data: SaveContactDto) {
+    try {
+      const jid = createJid(data.number);
+      await this.client.chatModify(
+        {
+          contact: {
+            fullName: data.name || 'Unknown',
+            firstName: (data.name || 'Unknown').split(' ')[0],
+            saveOnPrimaryAddressbook: data.saveOnDevice ?? true,
+          },
+        },
+        jid,
+      );
+
+      return { saved: true, number: data.number, name: data.name };
+    } catch (error) {
+      throw new InternalServerErrorException({
+        saved: false,
+        message: ['An error occurred while saving the contact.', error.toString()],
</code_context>

<issue_to_address>
**🚨 issue (security):** Avoid returning the raw error string in the API response to prevent leaking internal details.

The `InternalServerErrorException` payload currently includes `error.toString()` in the `message` array, which can reveal internal details to clients. Instead, log the original error on the server and return only a generic, user‑friendly message in the response body.
</issue_to_address>

### Comment 2
<location> `src/api/routes/contact.router.ts:22-24` </location>
<code_context>
+export class ContactRouter extends RouterBroker {
+  constructor(...guards: RequestHandler[]) {
+    super();
+    this.router.post(this.routerPath('save'), ...guards, async (req, res) => {
+      try {
+        const response = await this.dataValidate<SaveContactDto>({
+          request: req,
+          schema: saveContactSchema,
+          ClassRef: SaveContactDto,
+          execute: (instance, data) => contactController.saveContact(instance, data),
+        });
+
+        return res.status(HttpStatus.OK).json(response);
+      } catch (error) {
+        return res.status(HttpStatus.BAD_REQUEST).json(error);
+      }
+    });
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Differentiate between validation errors and server errors instead of always returning 400.

The `catch` block currently turns every error into `HttpStatus.BAD_REQUEST`, including actual server failures from calls like `waInstances[instanceName].saveContact`. This misclassifies errors for clients. Instead, only map known validation errors to 400 and default everything else to 500 (or use `error.status || HttpStatus.INTERNAL_SERVER_ERROR` when that metadata exists).

```suggestion
      } catch (error) {
        const err = error as any;

        const isValidationError =
          err?.name === 'ValidationError' ||
          err?.isJoi === true ||
          err?.status === HttpStatus.BAD_REQUEST;

        const status = isValidationError
          ? HttpStatus.BAD_REQUEST
          : err?.status || HttpStatus.INTERNAL_SERVER_ERROR;

        return res.status(status).json(err);
      }
```
</issue_to_address>

### Comment 3
<location> `src/validate/contact.schema.ts:4-12` </location>
<code_context>
+import { JSONSchema7 } from 'json-schema';
+import { v4 } from 'uuid';
+
+export const saveContactSchema: JSONSchema7 = {
+  $id: v4(),
+  type: 'object',
+  properties: {
+    number: { type: 'string' },
+    name: { type: 'string' },
+    saveOnDevice: { type: 'boolean', default: true },
+  },
+  required: ['number', 'name'],
+};
</code_context>

<issue_to_address>
**question (bug_risk):** Schema requirements and runtime behavior for `name` are inconsistent.

The schema makes `name` required, but `saveContact` in `BaileysStartupService` treats a missing `data.name` as valid and defaults to `'Unknown'`. Please align these: either make `name` optional (and/or add a default) in the schema, or remove the fallback so missing `name` is treated as a validation error.
</issue_to_address>

### Comment 4
<location> `src/validate/contact.schema.ts:1-5` </location>
<code_context>
+import { v4 } from 'uuid';
+
+export const saveContactSchema: JSONSchema7 = {
+  $id: v4(),
+  type: 'object',
+  properties: {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using a random UUID for `$id` can make schema identification and caching unstable.

Because `$id` is generated with `v4()`, it will change on every process start, which can break tools that rely on a stable `$id` for caching and cross‑schema references. Use a fixed, deterministic identifier (e.g., a URL‑like string) so the schema can be consistently referenced.

```suggestion
import { JSONSchema7 } from 'json-schema';

export const saveContactSchema: JSONSchema7 = {
  $id: 'https://your-app.example.com/schemas/save-contact.json',
```
</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 22 to 24
} catch (error) {
return res.status(HttpStatus.BAD_REQUEST).json(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 (bug_risk): Differentiate between validation errors and server errors instead of always returning 400.

The catch block currently turns every error into HttpStatus.BAD_REQUEST, including actual server failures from calls like waInstances[instanceName].saveContact. This misclassifies errors for clients. Instead, only map known validation errors to 400 and default everything else to 500 (or use error.status || HttpStatus.INTERNAL_SERVER_ERROR when that metadata exists).

Suggested change
} catch (error) {
return res.status(HttpStatus.BAD_REQUEST).json(error);
}
} catch (error) {
const err = error as any;
const isValidationError =
err?.name === 'ValidationError' ||
err?.isJoi === true ||
err?.status === HttpStatus.BAD_REQUEST;
const status = isValidationError
? HttpStatus.BAD_REQUEST
: err?.status || HttpStatus.INTERNAL_SERVER_ERROR;
return res.status(status).json(err);
}

Comment on lines 4 to 12
export const saveContactSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { type: 'string' },
name: { type: 'string' },
saveOnDevice: { type: 'boolean', default: true },
},
required: ['number', 'name'],
Copy link
Contributor

Choose a reason for hiding this comment

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

question (bug_risk): Schema requirements and runtime behavior for name are inconsistent.

The schema makes name required, but saveContact in BaileysStartupService treats a missing data.name as valid and defaults to 'Unknown'. Please align these: either make name optional (and/or add a default) in the schema, or remove the fallback so missing name is treated as a validation error.

Comment on lines 1 to 5
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

export const saveContactSchema: JSONSchema7 = {
$id: v4(),
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): Using a random UUID for $id can make schema identification and caching unstable.

Because $id is generated with v4(), it will change on every process start, which can break tools that rely on a stable $id for caching and cross‑schema references. Use a fixed, deterministic identifier (e.g., a URL‑like string) so the schema can be consistently referenced.

Suggested change
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
export const saveContactSchema: JSONSchema7 = {
$id: v4(),
import { JSONSchema7 } from 'json-schema';
export const saveContactSchema: JSONSchema7 = {
$id: 'https://your-app.example.com/schemas/save-contact.json',

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new endpoint to save contacts in WhatsApp, implementing support for the Baileys provider. The feature allows users to save contact information to their WhatsApp address book via the API.

Key changes:

  • New /contact/save endpoint with validation schema and routing
  • Implementation of contact saving logic in the Baileys service using the chatModify API
  • Complete integration including controller, DTO, router, and schema files

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/validate/contact.schema.ts Defines JSON schema validation for the save contact endpoint with number, name, and optional saveOnDevice fields
src/api/dto/contact.dto.ts Defines the SaveContactDto data transfer object matching the validation schema
src/api/controllers/contact.controller.ts Implements the contact controller that delegates to the WhatsApp service instance
src/api/routes/contact.router.ts Creates the Express router with the /save endpoint and request validation
src/api/server.module.ts Registers the new ContactController instance in the server module
src/api/routes/index.router.ts Mounts the ContactRouter at the /contact path
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts Implements the saveContact method using Baileys' chatModify API to save contacts to the address book

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

properties: {
number: { type: 'string' },
name: { type: 'string' },
saveOnDevice: { type: 'boolean', default: true },
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The 'default' keyword in JSON Schema is metadata only and won't be automatically applied by the jsonschema validation library used in this codebase. The default value is correctly handled in the Baileys service code using the nullish coalescing operator (??), so this schema property is misleading and should be removed to avoid confusion. If you want to document the default behavior, use a description field instead.

Suggested change
saveOnDevice: { type: 'boolean', default: true },
saveOnDevice: { type: 'boolean', description: 'Defaults to true when not provided.' },

Copilot uses AI. Check for mistakes.
} catch (error) {
throw new InternalServerErrorException({
saved: false,
message: ['An error occurred while saving the contact.', error.toString()],
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The error message uses 'An error occurred while saving the contact.' but looking at similar methods in the codebase (archiveChat at line 3739, markChatUnread at line 3767), they include more detailed guidance like 'Open a calling.' Consider adding more context to help users understand what action to take when this error occurs, following the pattern established in the codebase.

Suggested change
message: ['An error occurred while saving the contact.', error.toString()],
message: ['An error occurred while saving the contact.', 'Open a calling.', error.toString()],

Copilot uses AI. Check for mistakes.
Comment on lines 4 to 9

export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}

public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
return await this.waMonitor.waInstances[instanceName].saveContact(data);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The saveContact method is missing from the evolution and meta provider implementations. Looking at the pattern in evolution.channel.service.ts (lines 783-794) and whatsapp.business.service.ts (lines 1666-1680), methods that are not available in a provider should have stub implementations that throw a BadRequestException with a descriptive message. Without these stub methods, calling this endpoint with an evolution or meta instance will result in a runtime error ('saveContact is not a function') instead of a proper error response. Add stub implementations in both evolution.channel.service.ts and whatsapp.business.service.ts.

Suggested change
export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
return await this.waMonitor.waInstances[instanceName].saveContact(data);
import { BadRequestException } from '@nestjs/common';
export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
const instance = this.waMonitor.waInstances[instanceName];
if (!instance || typeof instance.saveContact !== 'function') {
throw new BadRequestException(
`saveContact is not supported for the provider used by instance "${instanceName}"`,
);
}
return await instance.saveContact(data);

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 24
this.router.post(this.routerPath('save'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<SaveContactDto>({
request: req,
schema: saveContactSchema,
ClassRef: SaveContactDto,
execute: (instance, data) => contactController.saveContact(instance, data),
});

return res.status(HttpStatus.OK).json(response);
} catch (error) {
return res.status(HttpStatus.BAD_REQUEST).json(error);
}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The try-catch block here is inconsistent with the codebase conventions. Most routers in the codebase (GroupRouter, LabelRouter, and most routes in ChatRouter) do not wrap dataValidate calls in try-catch blocks, relying instead on the 'express-async-errors' package imported in the abstract router to handle async errors automatically. The try-catch pattern is only used in a few specific cases in BusinessRouter where custom error handling is needed. For consistency, remove the try-catch block and let the framework handle errors automatically.

Copilot uses AI. Check for mistakes.
execute: (instance, data) => contactController.saveContact(instance, data),
});

return res.status(HttpStatus.OK).json(response);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The HTTP status code should be HttpStatus.CREATED instead of HttpStatus.OK for a create/save operation. This is consistent with similar operations in the codebase: CallRouter uses CREATED for offerCall, ChatRouter uses CREATED for archiveChat/markChatUnread, and GroupRouter uses CREATED for create operations. Since this endpoint is creating/saving a new contact, it should return a 201 CREATED status code.

Copilot uses AI. Check for mistakes.
Comment on lines 4 to 13
export const saveContactSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { type: 'string' },
name: { type: 'string' },
saveOnDevice: { type: 'boolean', default: true },
},
required: ['number', 'name'],
};
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The validation schema is missing the isNotEmpty validation pattern that is consistently used throughout the codebase for required string fields. Looking at chat.schema.ts and label.schema.ts, required string fields use both the 'required' array and the isNotEmpty helper to ensure they have a minLength of 1. Without this validation, the endpoint could accept empty strings for 'number' and 'name', which would be invalid. Add the isNotEmpty helper function and apply it to the required fields.

Copilot uses AI. Check for mistakes.
- Ajusta schema de validação: usa ID estático e melhora validação de campos.
- Atualiza controller: adiciona verificação de provedor.
- Melhora router: usa status 201 e aproveita global error handling.
- Adiciona métodos stub ausentes para provedores Evolution e Meta.
- Aprimora mensagens de erro no serviço Baileys.
@FaelN1 FaelN1 closed this Dec 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