diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index a1f289cf2b..6a561c0285 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -13,6 +13,7 @@ import { ProfileStatusDto, ReadMessageDto, SendPresenceDto, + UpdateContactDto, UpdateMessageDto, WhatsAppNumberDto, } from '@api/dto/chat.dto'; @@ -104,6 +105,10 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); } + public async updateContact({ instanceName }: InstanceDto, data: UpdateContactDto) { + return await this.waMonitor.waInstances[instanceName].updateContact(data); + } + public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) { return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status); } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 9c6be9f4c9..807153335d 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -51,6 +51,13 @@ export class ProfileNameDto { name: string; } +export class UpdateContactDto { + number: string; + fullName: string; + firstName?: string; + saveOnPrimaryAddressbook?: boolean; +} + export class ProfileStatusDto { status: string; } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 22839fd451..5f44568455 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -13,6 +13,7 @@ import { PrivacySettingDto, ReadMessageDto, SendPresenceDto, + UpdateContactDto, UpdateMessageDto, WhatsAppNumberDto, } from '@api/dto/chat.dto'; @@ -118,6 +119,7 @@ import makeWASocket, { isJidBroadcast, isJidGroup, isJidNewsletter, + isLidUser, isPnUser, jidNormalizedUser, makeCacheableSignalKeyStore, @@ -4541,6 +4543,75 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async updateContact(data: UpdateContactDto) { + const jid = createJid(data.number); + + if (isJidGroup(jid) || isJidNewsletter(jid) || isJidBroadcast(jid)) { + throw new BadRequestException('Only individual contacts can be updated'); + } + + let pnJid: string | null = null; + let lidJid: string | null = null; + + if (isLidUser(jid)) { + lidJid = jid; + } else { + const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + + if (!onWhatsapp?.exists) { + throw new BadRequestException(onWhatsapp); + } + + if (isLidUser(onWhatsapp.jid)) { + lidJid = onWhatsapp.jid; + } else { + pnJid = onWhatsapp.jid; + } + } + + const lidMapping = this.client.signalRepository.lidMapping; + if (!pnJid) { + const pn = await lidMapping.getPNForLID(lidJid); + pnJid = pn ? jidNormalizedUser(pn) : isPnUser(jid) ? jid : null; + } + if (!lidJid) { + const lid = await lidMapping.getLIDForPN(pnJid); + lidJid = lid ? jidNormalizedUser(lid) : null; + } + + if (!pnJid) { + throw new BadRequestException(`No phone number is known for ${lidJid}`); + } + + // Same shape the phone uses when it syncs a contact: the phone number JID + // goes in the patch index and the LID travels inside the action. + const contact: proto.SyncActionValue.IContactAction = { fullName: data.fullName }; + if (data.firstName) contact.firstName = data.firstName; + if (lidJid) contact.lidJid = lidJid; + if (data.saveOnPrimaryAddressbook !== undefined) { + contact.saveOnPrimaryAddressbook = data.saveOnPrimaryAddressbook; + } + + try { + await this.client.addOrEditContact(pnJid, contact); + } catch (error) { + throw new InternalServerErrorException('Error updating contact', error.toString()); + } + + // emitOwnEvents is off, so Baileys will not echo this change back to us. + await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]); + + // Chats that arrived addressed to the LID keep their own contact row. + if (lidJid && this.configService.get('DATABASE').SAVE_DATA.CONTACTS) { + await this.prismaRepository.contact.updateMany({ + where: { remoteJid: lidJid, instanceId: this.instanceId }, + data: { pushName: data.fullName }, + }); + } + + return { update: 'success', remoteJid: pnJid, lid: lidJid, fullName: data.fullName }; + } + public async updateProfileStatus(status: string) { try { await this.client.updateProfileStatus(status); diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 86f4d65bee..4764726906 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -14,6 +14,7 @@ import { ProfileStatusDto, ReadMessageDto, SendPresenceDto, + UpdateContactDto, UpdateMessageDto, WhatsAppNumberDto, } from '@api/dto/chat.dto'; @@ -39,6 +40,7 @@ import { profileSchema, profileStatusSchema, readMessageSchema, + updateContactSchema, updateMessageSchema, whatsappNumberSchema, } from '@validate/validate.schema'; @@ -257,6 +259,16 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .post(this.routerPath('updateContact'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: updateContactSchema, + ClassRef: UpdateContactDto, + execute: (instance, data) => chatController.updateContact(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { const response = await this.dataValidate({ request: req, diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index 6677b652fd..ebbb278ed6 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -319,6 +319,19 @@ export const profileNameSchema: JSONSchema7 = { ...isNotEmpty('name'), }; +export const updateContactSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + fullName: { type: 'string' }, + firstName: { type: 'string' }, + saveOnPrimaryAddressbook: { type: 'boolean' }, + }, + required: ['number', 'fullName'], + ...isNotEmpty('number', 'fullName'), +}; + export const profileStatusSchema: JSONSchema7 = { $id: v4(), type: 'object',