diff --git a/src/api/controllers/contact.controller.ts b/src/api/controllers/contact.controller.ts new file mode 100644 index 0000000000..d35b23543a --- /dev/null +++ b/src/api/controllers/contact.controller.ts @@ -0,0 +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) { + 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/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..b0f069e08b --- /dev/null +++ b/src/validate/contact.schema.ts @@ -0,0 +1,30 @@ +import { JSONSchema7 } from 'json-schema'; +import { v4 } from 'uuid'; + +export const saveContactSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + 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'], +}; 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';