-
Notifications
You must be signed in to change notification settings - Fork 5.1k
feat(contact): add save contact endpoint and Baileys handler #2340
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
Conversation
Reviewer's GuideAdds 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 flowsequenceDiagram
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 }
Updated class diagram for contact save flow typesclassDiagram
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
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this 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.routeris used inside the constructor before therouterfield is initialized (field initializers run aftersuper()and before the constructor body, in declaration order), so therouterproperty should be declared/initialized before the constructor or initialized inside the constructor to avoidundefinedat runtime. - The
ContactController.saveContactdirectly callsthis.waMonitor.waInstances[instanceName].saveContact, which assumes every waInstance implementssaveContact; 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
src/api/routes/contact.router.ts
Outdated
| } catch (error) { | ||
| return res.status(HttpStatus.BAD_REQUEST).json(error); | ||
| } |
There was a problem hiding this comment.
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).
| } 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); | |
| } |
src/validate/contact.schema.ts
Outdated
| export const saveContactSchema: JSONSchema7 = { | ||
| $id: v4(), | ||
| type: 'object', | ||
| properties: { | ||
| number: { type: 'string' }, | ||
| name: { type: 'string' }, | ||
| saveOnDevice: { type: 'boolean', default: true }, | ||
| }, | ||
| required: ['number', 'name'], |
There was a problem hiding this comment.
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.
src/validate/contact.schema.ts
Outdated
| import { JSONSchema7 } from 'json-schema'; | ||
| import { v4 } from 'uuid'; | ||
|
|
||
| export const saveContactSchema: JSONSchema7 = { | ||
| $id: v4(), |
There was a problem hiding this comment.
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.
| 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', |
There was a problem hiding this 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/saveendpoint with validation schema and routing - Implementation of contact saving logic in the Baileys service using the
chatModifyAPI - 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.
src/validate/contact.schema.ts
Outdated
| properties: { | ||
| number: { type: 'string' }, | ||
| name: { type: 'string' }, | ||
| saveOnDevice: { type: 'boolean', default: true }, |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| saveOnDevice: { type: 'boolean', default: true }, | |
| saveOnDevice: { type: 'boolean', description: 'Defaults to true when not provided.' }, |
| } catch (error) { | ||
| throw new InternalServerErrorException({ | ||
| saved: false, | ||
| message: ['An error occurred while saving the contact.', error.toString()], |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| message: ['An error occurred while saving the contact.', error.toString()], | |
| message: ['An error occurred while saving the contact.', 'Open a calling.', error.toString()], |
|
|
||
| export class ContactController { | ||
| constructor(private readonly waMonitor: WAMonitoringService) {} | ||
|
|
||
| public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) { | ||
| return await this.waMonitor.waInstances[instanceName].saveContact(data); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
| 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); |
src/api/routes/contact.router.ts
Outdated
| 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); | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
src/api/routes/contact.router.ts
Outdated
| execute: (instance, data) => contactController.saveContact(instance, data), | ||
| }); | ||
|
|
||
| return res.status(HttpStatus.OK).json(response); |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
src/validate/contact.schema.ts
Outdated
| export const saveContactSchema: JSONSchema7 = { | ||
| $id: v4(), | ||
| type: 'object', | ||
| properties: { | ||
| number: { type: 'string' }, | ||
| name: { type: 'string' }, | ||
| saveOnDevice: { type: 'boolean', default: true }, | ||
| }, | ||
| required: ['number', 'name'], | ||
| }; |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
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.
- 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.
📋 Descrição
Este PR introduz um novo endpoint para salvar contatos e implementa o handler correspondente na integração Baileys.
Principais alterações:
save(salvar) noContactController.WhatsAppBaileysService.ContactRouterpara incluir a nova rota.contact.schema.ts.ServerModuleeIndexRouter.🔗 Issue Relacionada
None
🧪 Tipo de Alteração
🧪 Testes
📸 Screenshots (se aplicável)
None
✅ Checklist
📝 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: