diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 71b5a743f0..16c42cac39 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -70,7 +70,7 @@ model Instance { integration String? @db.VarChar(100) number String? @db.VarChar(100) businessId String? @db.VarChar(100) - token String? @db.VarChar(255) + token String? @db.Text clientName String? @db.VarChar(100) disconnectionReasonCode Int? @db.Int disconnectionObject Json? @db.Json diff --git a/prisma/postgresql-migrations/20260714182607_20260714152530_change_type_instance_token/migration.sql b/prisma/postgresql-migrations/20260714182607_20260714152530_change_type_instance_token/migration.sql new file mode 100644 index 0000000000..16c4f7aae6 --- /dev/null +++ b/prisma/postgresql-migrations/20260714182607_20260714152530_change_type_instance_token/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Instance" ALTER COLUMN "token" SET DATA TYPE TEXT; diff --git a/prisma/postgresql-migrations/migration_lock.toml b/prisma/postgresql-migrations/migration_lock.toml index 648c57fd59..044d57cdb0 100644 --- a/prisma/postgresql-migrations/migration_lock.toml +++ b/prisma/postgresql-migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file +provider = "postgresql" diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 6b98f88da4..7fb40fd7c1 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -70,7 +70,7 @@ model Instance { integration String? @db.VarChar(100) number String? @db.VarChar(100) businessId String? @db.VarChar(100) - token String? @db.VarChar(255) + token String? @db.Text clientName String? @db.VarChar(100) disconnectionReasonCode Int? @db.Integer disconnectionObject Json? @db.JsonB diff --git a/src/api/integrations/channel/meta/meta.controller.ts b/src/api/integrations/channel/meta/meta.controller.ts index 558a22e98b..a886f5a315 100644 --- a/src/api/integrations/channel/meta/meta.controller.ts +++ b/src/api/integrations/channel/meta/meta.controller.ts @@ -15,54 +15,118 @@ export class MetaController extends ChannelController implements ChannelControll integrationEnabled: boolean; public async receiveWebhook(data: any) { - if (data.object === 'whatsapp_business_account') { - if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') { - const template = await this.prismaRepository.template.findFirst({ - where: { templateId: `${data.entry[0].changes[0].value.message_template_id}` }, - }); + if (data.object !== 'whatsapp_business_account') { + return { + status: 'success', + }; + } - if (!template) { - console.log('template not found'); - return; - } + const entries = data.entry ?? []; - const { webhookUrl } = template; + for (const entry of entries) { + const changes = entry.changes ?? []; - await axios.post(webhookUrl, data.entry[0].changes[0].value, { - headers: { - 'Content-Type': 'application/json', - }, - }); - return; - } + for (const change of changes) { + if (change?.field === 'message_template_status_update') { + const templateId = change?.value?.message_template_id; + + if (!templateId) { + this.logger.error('WebhookService -> receiveWebhookMeta -> templateId not found'); + continue; + } + + const template = await this.prismaRepository.template.findFirst({ + where: { + templateId: String(templateId), + }, + }); + + if (!template) { + this.logger.error(`WebhookService -> receiveWebhookMeta -> template not found: ${templateId}`); + continue; + } + + if (!template.webhookUrl) { + this.logger.error(`WebhookService -> receiveWebhookMeta -> template webhookUrl not found: ${templateId}`); + continue; + } + + try { + await axios.post(template.webhookUrl, change.value, { + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + this.logger.error( + `WebhookService -> receiveWebhookMeta -> error sending template webhook: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + + continue; + } - data.entry?.forEach(async (entry: any) => { - const numberId = entry.changes[0].value.metadata.phone_number_id; + const numberId = change?.value?.metadata?.phone_number_id; if (!numberId) { this.logger.error('WebhookService -> receiveWebhookMeta -> numberId not found'); - return { - status: 'success', - }; + continue; } - const instance = await this.prismaRepository.instance.findFirst({ - where: { number: numberId }, + const instances = await this.prismaRepository.instance.findMany({ + where: { + number: String(numberId), + }, }); - if (!instance) { - this.logger.error('WebhookService -> receiveWebhookMeta -> instance not found'); - return { - status: 'success', - }; + if (!instances.length) { + this.logger.error(`WebhookService -> receiveWebhookMeta -> instances not found for numberId: ${numberId}`); + continue; } - await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); - - return { - status: 'success', + const webhookData = { + ...data, + entry: [ + { + ...entry, + changes: [change], + }, + ], }; - }); + + const results = await Promise.allSettled( + instances.map(async (instance) => { + const waInstance = this.waMonitor.waInstances[instance.name]; + + if (!waInstance) { + throw new Error(`Instance not loaded: ${instance.name}`); + } + + await waInstance.connectToWhatsapp(webhookData); + + return instance.name; + }), + ); + + results.forEach((result, index) => { + const instanceName = instances[index].name; + + if (result.status === 'rejected') { + this.logger.error( + `WebhookService -> receiveWebhookMeta -> error processing webhook for instance ${instanceName}: ${ + result.reason instanceof Error ? result.reason.message : String(result.reason) + }`, + ); + return; + } + + this.logger.log( + `WebhookService -> receiveWebhookMeta -> webhook processed successfully for instance ${instanceName}`, + ); + }); + } } return { diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 1e4808c156..ca32165b76 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -126,17 +126,38 @@ export class BusinessStartupService extends ChannelStartupService { public async connectToWhatsapp(data?: any): Promise { if (!data) return; - const content = data.entry[0].changes[0].value; + const content = data?.entry?.[0]?.changes?.[0]?.value; + + if (!content) { + this.logger.error('ChannelStartupService -> connectToWhatsapp -> webhook content not found'); + return; + } try { - this.loadChatwoot(); + const message = Array.isArray(content.messages) ? content.messages[0] : undefined; + + const status = Array.isArray(content.statuses) ? content.statuses[0] : undefined; + + const messageEcho = Array.isArray(content.message_echoes) ? content.message_echoes[0] : undefined; + + const phoneNumber = message?.from ?? status?.recipient_id ?? messageEcho?.to; + + if (!phoneNumber) { + this.logger.error( + 'ChannelStartupService -> connectToWhatsapp -> phone number not found in messages, statuses or message_echoes', + ); + return; + } + + this.phoneNumber = createJid(phoneNumber); - this.eventHandler(content); + this.loadChatwoot(); - this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id); + await this.eventHandler(content); } catch (error) { this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); + + throw new InternalServerErrorException(error instanceof Error ? error.message : String(error)); } } @@ -387,7 +408,10 @@ export class BusinessStartupService extends ChannelStartupService { let messageRaw: any; let pushName: any; - if (received.contacts) pushName = received.contacts[0].profile.name; + const receivedContact = received.contacts[0]; + const remoteJid = receivedContact.profile?.phone || receivedContact.wa_id; + + if (received.contacts) pushName = receivedContact.profile?.name || ''; if (received.messages) { const message = received.messages[0]; // Añadir esta línea para definir message @@ -702,7 +726,7 @@ export class BusinessStartupService extends ChannelStartupService { }); const contactRaw: any = { - remoteJid: received.contacts[0].profile.phone, + remoteJid, pushName, // profilePicUrl: '', instanceId: this.instanceId, @@ -714,7 +738,7 @@ export class BusinessStartupService extends ChannelStartupService { if (contact) { const contactRaw: any = { - remoteJid: received.contacts[0].profile.phone, + remoteJid, pushName, // profilePicUrl: '', instanceId: this.instanceId, @@ -1016,7 +1040,8 @@ export class BusinessStartupService extends ChannelStartupService { return await this.post(content, 'messages'); } if (message['media']) { - const isImage = message['mimetype']?.startsWith('image/'); + const mimeType = this.normalizeMimeType(message['mimetype']); + const isImage = mimeType.startsWith('image/'); content = { messaging_product: 'whatsapp', @@ -1025,14 +1050,25 @@ export class BusinessStartupService extends ChannelStartupService { to: number.replace(/\D/g, ''), [message['mediaType']]: { [message['type']]: message['id'], + ...(message['mediaType'] !== 'audio' && message['mediaType'] !== 'video' && message['fileName'] && - !isImage && { filename: message['fileName'] }), - ...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }), + !isImage && { + filename: message['fileName'], + }), + + ...(message['mediaType'] !== 'audio' && + message['caption'] && { + caption: message['caption'], + }), }, }; - quoted ? (content.context = { message_id: quoted.id }) : content; + + if (quoted) { + content.context = { message_id: quoted.id }; + } + return await this.post(content, 'messages'); } if (message['audio']) { @@ -1174,6 +1210,35 @@ export class BusinessStartupService extends ChannelStartupService { } } + private normalizeMimeType(value: unknown): string { + if (typeof value === 'string') { + return value; + } + + if (Array.isArray(value)) { + const mimeType = value.find((item) => typeof item === 'string'); + return typeof mimeType === 'string' ? mimeType : ''; + } + + if (value && typeof value === 'object') { + const mimeTypeObject = value as Record; + + const candidates = [ + mimeTypeObject.mimetype, + mimeTypeObject.mimeType, + mimeTypeObject.contentType, + mimeTypeObject.content_type, + mimeTypeObject.type, + ]; + + const mimeType = candidates.find((item) => typeof item === 'string'); + + return typeof mimeType === 'string' ? mimeType : ''; + } + + return ''; + } + // Send Message Controller public async textMessage(data: SendTextDto, isIntegration = false) { const res = await this.sendMessageWithTyping(