From a2803629afee69ab93ed27fcc6752bb0091f3bc5 Mon Sep 17 00:00:00 2001 From: Soham Date: Wed, 26 Aug 2026 11:47:17 +0530 Subject: [PATCH 1/2] feat(contact): add contact save endpoint for Baileys provider (#2711) - Add SaveContactDto for typed contact payload - Add saveContactSchema with validation for number and name - Add ContactController delegating to active Baileys instance - Add ContactRouter mounting POST /contact/save/:instanceName - Register ContactRouter in index.router.ts and server.module.ts - Implement saveContact in BaileysStartupService using chatModify contact app-state sync --- src/api/controllers/contact.controller.ts | 11 +++++ src/api/dto/contact.dto.ts | 6 +++ .../whatsapp/whatsapp.baileys.service.ts | 48 +++++++++++++++++++ src/api/routes/contact.router.ts | 25 ++++++++++ src/api/routes/index.router.ts | 2 + src/api/server.module.ts | 2 + src/validate/contact.schema.ts | 34 +++++++++++++ src/validate/validate.schema.ts | 1 + 8 files changed, 129 insertions(+) create mode 100644 src/api/controllers/contact.controller.ts create mode 100644 src/api/dto/contact.dto.ts create mode 100644 src/api/routes/contact.router.ts create mode 100644 src/validate/contact.schema.ts diff --git a/src/api/controllers/contact.controller.ts b/src/api/controllers/contact.controller.ts new file mode 100644 index 0000000000..243b260cae --- /dev/null +++ b/src/api/controllers/contact.controller.ts @@ -0,0 +1,11 @@ +import { SaveContactDto } from '@api/dto/contact.dto'; +import { InstanceDto } from '@api/dto/instance.dto'; +import { WAMonitoringService } from '@api/services/monitor.service'; + +export class ContactController { + constructor(private readonly waMonitor: WAMonitoringService) {} + + public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) { + return await this.waMonitor.waInstances[instanceName].saveContact(data); + } +} diff --git a/src/api/dto/contact.dto.ts b/src/api/dto/contact.dto.ts new file mode 100644 index 0000000000..50a50571df --- /dev/null +++ b/src/api/dto/contact.dto.ts @@ -0,0 +1,6 @@ +export class SaveContactDto { + number: string; + name: string; + firstName?: string; + saveOnDevice?: boolean; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc1..ebd883669d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -15,6 +15,7 @@ import { UpdateMessageDto, WhatsAppNumberDto, } from '@api/dto/chat.dto'; +import { SaveContactDto } from '@api/dto/contact.dto'; import { AcceptGroupInvite, CreateGroupDto, @@ -3768,6 +3769,53 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async saveContact(data: SaveContactDto) { + try { + const jid = createJid(data.number); + const firstName = data.firstName || data.name.trim().split(' ')[0]; + + await this.client.chatModify( + { + contact: { + fullName: data.name, + firstName: firstName, + saveOnPrimaryAddressbook: data.saveOnDevice ?? true, + }, + }, + jid, + ); + + if (this.configService.get('DATABASE').SAVE_DATA.CONTACTS) { + await this.prismaRepository.contact.upsert({ + where: { remoteJid_instanceId: { remoteJid: jid, instanceId: this.instanceId } }, + create: { + remoteJid: jid, + pushName: data.name, + profilePicUrl: null, + instanceId: this.instanceId, + }, + update: { + pushName: data.name, + }, + }); + } + + return { + saved: true, + number: data.number, + name: data.name, + firstName: firstName, + saveOnDevice: data.saveOnDevice ?? true, + }; + } catch (error) { + this.logger.error(`Error saving contact: ${error}`); + throw new InternalServerErrorException({ + saved: false, + message: ['Failed to save contact to WhatsApp.', error?.message || error.toString()], + }); + } + } + public async deleteMessage(del: DeleteMessage) { try { const response = await this.client.sendMessage(del.remoteJid, { delete: del }); diff --git a/src/api/routes/contact.router.ts b/src/api/routes/contact.router.ts new file mode 100644 index 0000000000..98d25da1e6 --- /dev/null +++ b/src/api/routes/contact.router.ts @@ -0,0 +1,25 @@ +import { RouterBroker } from '@api/abstract/abstract.router'; +import { SaveContactDto } from '@api/dto/contact.dto'; +import { contactController } from '@api/server.module'; +import { saveContactSchema } from '@validate/validate.schema'; +import { RequestHandler, Router } from 'express'; + +import { HttpStatus } from './index.router'; + +export class ContactRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router.post(this.routerPath('save'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: saveContactSchema, + ClassRef: SaveContactDto, + execute: (instance, data) => contactController.saveContact(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }); + } + + public readonly router: Router = Router(); +} diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 45c43fca5b..c2d83869ed 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -16,6 +16,7 @@ import path from 'path'; import { BusinessRouter } from './business.router'; import { CallRouter } from './call.router'; import { ChatRouter } from './chat.router'; +import { ContactRouter } from './contact.router'; import { GroupRouter } from './group.router'; import { InstanceRouter } from './instance.router'; import { LabelRouter } from './label.router'; @@ -218,6 +219,7 @@ router .use('/message', new MessageRouter(...guards).router) .use('/call', new CallRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router) + .use('/contact', new ContactRouter(...guards).router) .use('/business', new BusinessRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) .use('/template', new TemplateRouter(configService, ...guards).router) diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 668b9e2722..96db7bc2e6 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -6,6 +6,7 @@ import { Logger } from '@config/logger.config'; import { BusinessController } from './controllers/business.controller'; import { CallController } from './controllers/call.controller'; import { ChatController } from './controllers/chat.controller'; +import { ContactController } from './controllers/contact.controller'; import { GroupController } from './controllers/group.controller'; import { InstanceController } from './controllers/instance.controller'; import { LabelController } from './controllers/label.controller'; @@ -103,6 +104,7 @@ export const instanceController = new InstanceController( export const sendMessageController = new SendMessageController(waMonitor); export const callController = new CallController(waMonitor); export const chatController = new ChatController(waMonitor); +export const contactController = new ContactController(waMonitor); export const businessController = new BusinessController(waMonitor); export const groupController = new GroupController(waMonitor); export const labelController = new LabelController(waMonitor); diff --git a/src/validate/contact.schema.ts b/src/validate/contact.schema.ts new file mode 100644 index 0000000000..84591f1edb --- /dev/null +++ b/src/validate/contact.schema.ts @@ -0,0 +1,34 @@ +import { JSONSchema7 } from 'json-schema'; +import { v4 } from 'uuid'; + +const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; +}; + +export const saveContactSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + name: { type: 'string' }, + firstName: { type: 'string' }, + saveOnDevice: { type: 'boolean' }, + }, + required: ['number', 'name'], + ...isNotEmpty('number', 'name'), +}; diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 69e0090d83..17ed20ac4d 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -1,6 +1,7 @@ // Integrations Schema export * from './business.schema'; export * from './chat.schema'; +export * from './contact.schema'; export * from './group.schema'; export * from './instance.schema'; export * from './label.schema'; From eacfbc899594be5a03a167721d1378174282dd31 Mon Sep 17 00:00:00 2001 From: Soham Date: Wed, 26 Aug 2026 12:48:34 +0530 Subject: [PATCH 2/2] refactor(contact): address review feedback for number validation and provider checks - Add numeric regex pattern and length constraints to number in saveContactSchema - Replace conditional isNotEmpty with direct minLength constraints - Add unsupported-provider guard in ContactController with BadRequestException --- src/api/controllers/contact.controller.ts | 8 ++++- src/validate/contact.schema.ts | 44 +++++++++++------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/api/controllers/contact.controller.ts b/src/api/controllers/contact.controller.ts index 243b260cae..d35b23543a 100644 --- a/src/api/controllers/contact.controller.ts +++ b/src/api/controllers/contact.controller.ts @@ -1,11 +1,17 @@ import { SaveContactDto } from '@api/dto/contact.dto'; import { InstanceDto } from '@api/dto/instance.dto'; import { WAMonitoringService } from '@api/services/monitor.service'; +import { BadRequestException } from '@exceptions'; export class ContactController { constructor(private readonly waMonitor: WAMonitoringService) {} public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) { - return await this.waMonitor.waInstances[instanceName].saveContact(data); + const instance = this.waMonitor.waInstances[instanceName]; + if (!instance || typeof instance.saveContact !== 'function') { + throw new BadRequestException('Contact creation is only supported on WhatsApp Baileys instances.'); + } + + return await instance.saveContact(data); } } diff --git a/src/validate/contact.schema.ts b/src/validate/contact.schema.ts index 84591f1edb..b0f069e08b 100644 --- a/src/validate/contact.schema.ts +++ b/src/validate/contact.schema.ts @@ -1,34 +1,30 @@ import { JSONSchema7 } from 'json-schema'; import { v4 } from 'uuid'; -const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { - const properties = {}; - propertyNames.forEach( - (property) => - (properties[property] = { - minLength: 1, - description: `The "${property}" cannot be empty`, - }), - ); - return { - if: { - propertyNames: { - enum: [...propertyNames], - }, - }, - then: { properties }, - }; -}; - export const saveContactSchema: JSONSchema7 = { $id: v4(), type: 'object', properties: { - number: { type: 'string' }, - name: { type: 'string' }, - firstName: { type: 'string' }, - saveOnDevice: { type: 'boolean' }, + number: { + type: 'string', + minLength: 8, + maxLength: 20, + pattern: '^[0-9]+$', + description: 'The "number" must contain only digits (8-20 characters)', + }, + name: { + type: 'string', + minLength: 1, + maxLength: 100, + description: 'The "name" cannot be empty', + }, + firstName: { + type: 'string', + maxLength: 50, + }, + saveOnDevice: { + type: 'boolean', + }, }, required: ['number', 'name'], - ...isNotEmpty('number', 'name'), };