mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 07:04:50 -06:00

- Updated the token retrieval in the BusinessStartupService to use a class property instead of a hardcoded environment variable. - This change enhances flexibility and maintainability of the service configuration.
1736 lines
56 KiB
TypeScript
1736 lines
56 KiB
TypeScript
import { NumberBusiness } from '@api/dto/chat.dto';
|
|
import {
|
|
ContactMessage,
|
|
MediaMessage,
|
|
Options,
|
|
SendAudioDto,
|
|
SendButtonsDto,
|
|
SendContactDto,
|
|
SendListDto,
|
|
SendLocationDto,
|
|
SendMediaDto,
|
|
SendReactionDto,
|
|
SendTemplateDto,
|
|
SendTextDto,
|
|
} from '@api/dto/sendMessage.dto';
|
|
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
|
import { ProviderFiles } from '@api/provider/sessions';
|
|
import { PrismaRepository } from '@api/repository/repository.service';
|
|
import { chatbotController } from '@api/server.module';
|
|
import { CacheService } from '@api/services/cache.service';
|
|
import { ChannelStartupService } from '@api/services/channel.service';
|
|
import { Events, wa } from '@api/types/wa.types';
|
|
import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
|
|
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
|
import { createJid } from '@utils/createJid';
|
|
import { status } from '@utils/renderStatus';
|
|
import axios from 'axios';
|
|
import { arrayUnique, isURL } from 'class-validator';
|
|
import EventEmitter2 from 'eventemitter2';
|
|
import FormData from 'form-data';
|
|
import mimeTypes from 'mime-types';
|
|
import { join } from 'path';
|
|
|
|
export class BusinessStartupService extends ChannelStartupService {
|
|
constructor(
|
|
public readonly configService: ConfigService,
|
|
public readonly eventEmitter: EventEmitter2,
|
|
public readonly prismaRepository: PrismaRepository,
|
|
public readonly cache: CacheService,
|
|
public readonly chatwootCache: CacheService,
|
|
public readonly baileysCache: CacheService,
|
|
private readonly providerFiles: ProviderFiles,
|
|
) {
|
|
super(configService, eventEmitter, prismaRepository, chatwootCache);
|
|
}
|
|
|
|
public stateConnection: wa.StateConnection = { state: 'open' };
|
|
|
|
public phoneNumber: string;
|
|
public mobile: boolean;
|
|
|
|
public get connectionStatus() {
|
|
return this.stateConnection;
|
|
}
|
|
|
|
public async closeClient() {
|
|
this.stateConnection = { state: 'close' };
|
|
}
|
|
|
|
public get qrCode(): wa.QrCode {
|
|
return {
|
|
pairingCode: this.instance.qrcode?.pairingCode,
|
|
code: this.instance.qrcode?.code,
|
|
base64: this.instance.qrcode?.base64,
|
|
count: this.instance.qrcode?.count,
|
|
};
|
|
}
|
|
|
|
public async logoutInstance() {
|
|
await this.closeClient();
|
|
}
|
|
|
|
private isMediaMessage(message: any) {
|
|
return message.document || message.image || message.audio || message.video;
|
|
}
|
|
|
|
private async post(message: any, params: string) {
|
|
try {
|
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
|
urlServer = `${urlServer}/${version}/${this.number}/${params}`;
|
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
|
const result = await axios.post(urlServer, message, { headers });
|
|
return result.data;
|
|
} catch (e) {
|
|
return e.response?.data?.error;
|
|
}
|
|
}
|
|
|
|
public async profilePicture(number: string) {
|
|
const jid = createJid(number);
|
|
|
|
return {
|
|
wuid: jid,
|
|
profilePictureUrl: null,
|
|
};
|
|
}
|
|
|
|
public async getProfileName() {
|
|
return null;
|
|
}
|
|
|
|
public async profilePictureUrl() {
|
|
return null;
|
|
}
|
|
|
|
public async getProfileStatus() {
|
|
return null;
|
|
}
|
|
|
|
public async setWhatsappBusinessProfile(data: NumberBusiness): Promise<any> {
|
|
const content = {
|
|
messaging_product: 'whatsapp',
|
|
about: data.about,
|
|
address: data.address,
|
|
description: data.description,
|
|
vertical: data.vertical,
|
|
email: data.email,
|
|
websites: data.websites,
|
|
profile_picture_handle: data.profilehandle,
|
|
};
|
|
return await this.post(content, 'whatsapp_business_profile');
|
|
}
|
|
|
|
public async connectToWhatsapp(data?: any): Promise<any> {
|
|
if (!data) return;
|
|
|
|
const content = data.entry[0].changes[0].value;
|
|
|
|
try {
|
|
this.loadChatwoot();
|
|
|
|
this.eventHandler(content);
|
|
|
|
this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException(error?.toString());
|
|
}
|
|
}
|
|
|
|
private async downloadMediaMessage(message: any) {
|
|
try {
|
|
const id = message[message.type].id;
|
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
|
urlServer = `${urlServer}/${version}/${id}`;
|
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
|
|
|
// Primeiro, obtenha a URL do arquivo
|
|
let result = await axios.get(urlServer, { headers });
|
|
|
|
// Depois, baixe o arquivo usando a URL retornada
|
|
result = await axios.get(result.data.url, {
|
|
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
|
responseType: 'arraybuffer',
|
|
});
|
|
|
|
return result.data;
|
|
} catch (e) {
|
|
this.logger.error(`Error downloading media: ${e}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private messageMediaJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = message.type + 'Message';
|
|
content = { [content]: message[message.type] };
|
|
if (message.context) {
|
|
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
|
}
|
|
return content;
|
|
}
|
|
|
|
private messageAudioJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = {
|
|
audioMessage: {
|
|
...message.audio,
|
|
ptt: message.audio.voice || false, // Define se é mensagem de voz
|
|
},
|
|
};
|
|
if (message.context) {
|
|
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
|
}
|
|
return content;
|
|
}
|
|
|
|
private messageInteractiveJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = { conversation: message.interactive[message.interactive.type].title };
|
|
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
|
return content;
|
|
}
|
|
|
|
private messageButtonJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = { conversation: received.messages[0].button?.text };
|
|
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
|
return content;
|
|
}
|
|
|
|
private messageReactionJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = {
|
|
reactionMessage: {
|
|
key: {
|
|
id: message.reaction.message_id,
|
|
},
|
|
text: message.reaction.emoji,
|
|
},
|
|
};
|
|
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
|
return content;
|
|
}
|
|
|
|
private messageTextJson(received: any) {
|
|
// Verificar que received y received.messages existen
|
|
if (!received || !received.messages || received.messages.length === 0) {
|
|
this.logger.error('Error: received object or messages array is undefined or empty');
|
|
return null;
|
|
}
|
|
|
|
const message = received.messages[0];
|
|
let content: any;
|
|
|
|
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
|
|
if (!message.text) {
|
|
// Si no hay texto, manejamos diferente según el tipo de mensaje
|
|
if (message.type === 'sticker') {
|
|
content = { stickerMessage: {} };
|
|
} else if (message.type === 'location') {
|
|
content = {
|
|
locationMessage: {
|
|
degreesLatitude: message.location?.latitude,
|
|
degreesLongitude: message.location?.longitude,
|
|
name: message.location?.name,
|
|
address: message.location?.address,
|
|
},
|
|
};
|
|
} else {
|
|
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
|
|
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
|
|
content = { [message.type + 'Message']: message[message.type] || {} };
|
|
}
|
|
|
|
// Añadir contexto si existe
|
|
if (message.context) {
|
|
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
// Si el mensaje tiene texto, procesamos normalmente
|
|
if (!received.metadata || !received.metadata.phone_number_id) {
|
|
this.logger.error('Error: metadata or phone_number_id is undefined');
|
|
return null;
|
|
}
|
|
|
|
if (message.from === received.metadata.phone_number_id) {
|
|
content = {
|
|
extendedTextMessage: { text: message.text.body },
|
|
};
|
|
if (message.context) {
|
|
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
|
}
|
|
} else {
|
|
content = { conversation: message.text.body };
|
|
if (message.context) {
|
|
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
|
}
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
private messageLocationJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = {
|
|
locationMessage: {
|
|
degreesLatitude: message.location.latitude,
|
|
degreesLongitude: message.location.longitude,
|
|
name: message.location?.name,
|
|
address: message.location?.address,
|
|
},
|
|
};
|
|
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
|
return content;
|
|
}
|
|
|
|
private messageContactsJson(received: any) {
|
|
const message = received.messages[0];
|
|
let content: any = {};
|
|
|
|
const vcard = (contact: any) => {
|
|
let result =
|
|
'BEGIN:VCARD\n' +
|
|
'VERSION:3.0\n' +
|
|
`N:${contact.name.formatted_name}\n` +
|
|
`FN:${contact.name.formatted_name}\n`;
|
|
|
|
if (contact.org) {
|
|
result += `ORG:${contact.org.company};\n`;
|
|
}
|
|
|
|
if (contact.emails) {
|
|
result += `EMAIL:${contact.emails[0].email}\n`;
|
|
}
|
|
|
|
if (contact.urls) {
|
|
result += `URL:${contact.urls[0].url}\n`;
|
|
}
|
|
|
|
if (!contact.phones[0]?.wa_id) {
|
|
contact.phones[0].wa_id = createJid(contact.phones[0].phone);
|
|
}
|
|
|
|
result +=
|
|
`item1.TEL;waid=${contact.phones[0]?.wa_id}:${contact.phones[0].phone}\n` +
|
|
'item1.X-ABLabel:Celular\n' +
|
|
'END:VCARD';
|
|
|
|
return result;
|
|
};
|
|
|
|
if (message.contacts.length === 1) {
|
|
content.contactMessage = {
|
|
displayName: message.contacts[0].name.formatted_name,
|
|
vcard: vcard(message.contacts[0]),
|
|
};
|
|
} else {
|
|
content.contactsArrayMessage = {
|
|
displayName: `${message.length} contacts`,
|
|
contacts: message.map((contact) => {
|
|
return {
|
|
displayName: contact.name.formatted_name,
|
|
vcard: vcard(contact),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
|
return content;
|
|
}
|
|
|
|
private renderMessageType(type: string) {
|
|
let messageType: string;
|
|
|
|
switch (type) {
|
|
case 'text':
|
|
messageType = 'conversation';
|
|
break;
|
|
case 'image':
|
|
messageType = 'imageMessage';
|
|
break;
|
|
case 'video':
|
|
messageType = 'videoMessage';
|
|
break;
|
|
case 'audio':
|
|
messageType = 'audioMessage';
|
|
break;
|
|
case 'document':
|
|
messageType = 'documentMessage';
|
|
break;
|
|
case 'template':
|
|
messageType = 'conversation';
|
|
break;
|
|
case 'location':
|
|
messageType = 'locationMessage';
|
|
break;
|
|
case 'sticker':
|
|
messageType = 'stickerMessage';
|
|
break;
|
|
default:
|
|
messageType = 'conversation';
|
|
break;
|
|
}
|
|
|
|
return messageType;
|
|
}
|
|
|
|
protected async messageHandle(received: any, database: Database, settings: any) {
|
|
try {
|
|
let messageRaw: any;
|
|
let pushName: any;
|
|
|
|
if (received.contacts) pushName = received.contacts[0].profile.name;
|
|
|
|
if (received.messages) {
|
|
const message = received.messages[0]; // Añadir esta línea para definir message
|
|
|
|
const key = {
|
|
id: message.id,
|
|
remoteJid: this.phoneNumber,
|
|
fromMe: message.from === received.metadata.phone_number_id,
|
|
};
|
|
|
|
if (message.type === 'sticker') {
|
|
this.logger.log('Procesando mensaje de tipo sticker');
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: {
|
|
stickerMessage: message.sticker || {},
|
|
},
|
|
messageType: 'stickerMessage',
|
|
messageTimestamp: parseInt(message.timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
} else if (this.isMediaMessage(message)) {
|
|
const messageContent =
|
|
message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
|
|
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: messageContent,
|
|
contextInfo: messageContent?.contextInfo,
|
|
messageType: this.renderMessageType(received.messages[0].type),
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
if (this.configService.get<S3>('S3').ENABLE) {
|
|
try {
|
|
const message: any = received;
|
|
|
|
const id = message.messages[0][message.messages[0].type].id;
|
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
|
urlServer = `${urlServer}/${version}/${id}`;
|
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
|
const result = await axios.get(urlServer, { headers });
|
|
|
|
const buffer = await axios.get(result.data.url, {
|
|
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
|
responseType: 'arraybuffer',
|
|
});
|
|
|
|
let mediaType;
|
|
|
|
if (message.messages[0].document) {
|
|
mediaType = 'document';
|
|
} else if (message.messages[0].image) {
|
|
mediaType = 'image';
|
|
} else if (message.messages[0].audio) {
|
|
mediaType = 'audio';
|
|
} else {
|
|
mediaType = 'video';
|
|
}
|
|
|
|
const mimetype = result.data?.mime_type || result.headers['content-type'];
|
|
|
|
const contentDisposition = result.headers['content-disposition'];
|
|
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="(.+?)"/);
|
|
if (match) {
|
|
fileName = match[1];
|
|
}
|
|
}
|
|
|
|
// Para áudio, garantir extensão correta baseada no mimetype
|
|
if (mediaType === 'audio') {
|
|
if (mimetype.includes('ogg')) {
|
|
fileName = `${message.messages[0].id}.ogg`;
|
|
} else if (mimetype.includes('mp3')) {
|
|
fileName = `${message.messages[0].id}.mp3`;
|
|
} else if (mimetype.includes('m4a')) {
|
|
fileName = `${message.messages[0].id}.m4a`;
|
|
}
|
|
}
|
|
|
|
const size = result.headers['content-length'] || buffer.data.byteLength;
|
|
|
|
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
|
|
|
await s3Service.uploadFile(fullName, buffer.data, size, {
|
|
'Content-Type': mimetype,
|
|
});
|
|
|
|
const createdMessage = await this.prismaRepository.message.create({
|
|
data: messageRaw,
|
|
});
|
|
|
|
await this.prismaRepository.media.create({
|
|
data: {
|
|
messageId: createdMessage.id,
|
|
instanceId: this.instanceId,
|
|
type: mediaType,
|
|
fileName: fullName,
|
|
mimetype,
|
|
},
|
|
});
|
|
|
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
|
|
|
messageRaw.message.mediaUrl = mediaUrl;
|
|
messageRaw.message.base64 = buffer.data.toString('base64');
|
|
|
|
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
|
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
},
|
|
include: {
|
|
OpenaiCreds: true,
|
|
},
|
|
});
|
|
|
|
if (
|
|
openAiDefaultSettings &&
|
|
openAiDefaultSettings.openaiCredsId &&
|
|
openAiDefaultSettings.speechToText
|
|
) {
|
|
try {
|
|
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
|
openAiDefaultSettings.OpenaiCreds,
|
|
{
|
|
message: {
|
|
mediaUrl: messageRaw.message.mediaUrl,
|
|
...messageRaw,
|
|
},
|
|
},
|
|
)}`;
|
|
} catch (speechError) {
|
|
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
|
}
|
|
} else {
|
|
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
|
messageRaw.message.base64 = buffer.toString('base64');
|
|
|
|
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
|
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
},
|
|
include: {
|
|
OpenaiCreds: true,
|
|
},
|
|
});
|
|
|
|
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
|
try {
|
|
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
|
openAiDefaultSettings.OpenaiCreds,
|
|
{
|
|
message: {
|
|
base64: messageRaw.message.base64,
|
|
...messageRaw,
|
|
},
|
|
},
|
|
)}`;
|
|
} catch (speechError) {
|
|
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (received?.messages[0].interactive) {
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: {
|
|
...this.messageInteractiveJson(received),
|
|
},
|
|
contextInfo: this.messageInteractiveJson(received)?.contextInfo,
|
|
messageType: 'interactiveMessage',
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
} else if (received?.messages[0].button) {
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: {
|
|
...this.messageButtonJson(received),
|
|
},
|
|
contextInfo: this.messageButtonJson(received)?.contextInfo,
|
|
messageType: 'buttonMessage',
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
} else if (received?.messages[0].reaction) {
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: {
|
|
...this.messageReactionJson(received),
|
|
},
|
|
contextInfo: this.messageReactionJson(received)?.contextInfo,
|
|
messageType: 'reactionMessage',
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
} else if (received?.messages[0].contacts) {
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: {
|
|
...this.messageContactsJson(received),
|
|
},
|
|
contextInfo: this.messageContactsJson(received)?.contextInfo,
|
|
messageType: 'contactMessage',
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
} else {
|
|
messageRaw = {
|
|
key,
|
|
pushName,
|
|
message: this.messageTextJson(received),
|
|
contextInfo: this.messageTextJson(received)?.contextInfo,
|
|
messageType: this.renderMessageType(received.messages[0].type),
|
|
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
|
source: 'unknown',
|
|
instanceId: this.instanceId,
|
|
};
|
|
}
|
|
|
|
if (this.localSettings.readMessages) {
|
|
// await this.client.readMessages([received.key]);
|
|
}
|
|
|
|
this.logger.log(messageRaw);
|
|
|
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
|
|
|
await chatbotController.emit({
|
|
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
|
remoteJid: messageRaw.key.remoteJid,
|
|
msg: messageRaw,
|
|
pushName: messageRaw.pushName,
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
|
|
Events.MESSAGES_UPSERT,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
messageRaw,
|
|
);
|
|
|
|
if (chatwootSentMessage?.id) {
|
|
messageRaw.chatwootMessageId = chatwootSentMessage.id;
|
|
messageRaw.chatwootInboxId = chatwootSentMessage.id;
|
|
messageRaw.chatwootConversationId = chatwootSentMessage.id;
|
|
}
|
|
}
|
|
|
|
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
|
|
await this.prismaRepository.message.create({
|
|
data: messageRaw,
|
|
});
|
|
}
|
|
|
|
const contact = await this.prismaRepository.contact.findFirst({
|
|
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
|
|
});
|
|
|
|
const contactRaw: any = {
|
|
remoteJid: received.contacts[0].profile.phone,
|
|
pushName,
|
|
// profilePicUrl: '',
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
if (contactRaw.remoteJid === 'status@broadcast') {
|
|
return;
|
|
}
|
|
|
|
if (contact) {
|
|
const contactRaw: any = {
|
|
remoteJid: received.contacts[0].profile.phone,
|
|
pushName,
|
|
// profilePicUrl: '',
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
await this.chatwootService.eventWhatsapp(
|
|
Events.CONTACTS_UPDATE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
contactRaw,
|
|
);
|
|
}
|
|
|
|
await this.prismaRepository.contact.updateMany({
|
|
where: { remoteJid: contact.remoteJid },
|
|
data: contactRaw,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
|
|
|
|
this.prismaRepository.contact.create({
|
|
data: contactRaw,
|
|
});
|
|
}
|
|
if (received.statuses) {
|
|
for await (const item of received.statuses) {
|
|
const key = {
|
|
id: item.id,
|
|
remoteJid: this.phoneNumber,
|
|
fromMe: this.phoneNumber === received.metadata.phone_number_id,
|
|
};
|
|
if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) {
|
|
return;
|
|
}
|
|
if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) {
|
|
const findMessage = await this.prismaRepository.message.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
key: {
|
|
path: ['id'],
|
|
equals: key.id,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!findMessage) {
|
|
return;
|
|
}
|
|
|
|
if (item.message === null && item.status === undefined) {
|
|
this.sendDataWebhook(Events.MESSAGES_DELETE, key);
|
|
|
|
const message: any = {
|
|
messageId: findMessage.id,
|
|
keyId: key.id,
|
|
remoteJid: key.remoteJid,
|
|
fromMe: key.fromMe,
|
|
participant: key?.remoteJid,
|
|
status: 'DELETED',
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: message,
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.MESSAGES_DELETE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{ key: key },
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const message: any = {
|
|
messageId: findMessage.id,
|
|
keyId: key.id,
|
|
remoteJid: key.remoteJid,
|
|
fromMe: key.fromMe,
|
|
participant: key?.remoteJid,
|
|
status: item.status.toUpperCase(),
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
|
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: message,
|
|
});
|
|
|
|
if (findMessage.webhookUrl) {
|
|
await axios.post(findMessage.webhookUrl, message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
}
|
|
}
|
|
|
|
private convertMessageToRaw(message: any, content: any) {
|
|
let convertMessage: any;
|
|
|
|
if (message?.conversation) {
|
|
if (content?.context?.message_id) {
|
|
convertMessage = {
|
|
...message,
|
|
contextInfo: { stanzaId: content.context.message_id },
|
|
};
|
|
return convertMessage;
|
|
}
|
|
convertMessage = message;
|
|
return convertMessage;
|
|
}
|
|
|
|
if (message?.mediaType === 'image') {
|
|
if (content?.context?.message_id) {
|
|
convertMessage = {
|
|
imageMessage: message,
|
|
contextInfo: { stanzaId: content.context.message_id },
|
|
};
|
|
return convertMessage;
|
|
}
|
|
return {
|
|
imageMessage: message,
|
|
};
|
|
}
|
|
|
|
if (message?.mediaType === 'video') {
|
|
if (content?.context?.message_id) {
|
|
convertMessage = {
|
|
videoMessage: message,
|
|
contextInfo: { stanzaId: content.context.message_id },
|
|
};
|
|
return convertMessage;
|
|
}
|
|
return {
|
|
videoMessage: message,
|
|
};
|
|
}
|
|
|
|
if (message?.mediaType === 'audio') {
|
|
if (content?.context?.message_id) {
|
|
convertMessage = {
|
|
audioMessage: message,
|
|
contextInfo: { stanzaId: content.context.message_id },
|
|
};
|
|
return convertMessage;
|
|
}
|
|
return {
|
|
audioMessage: message,
|
|
};
|
|
}
|
|
|
|
if (message?.mediaType === 'document') {
|
|
if (content?.context?.message_id) {
|
|
convertMessage = {
|
|
documentMessage: message,
|
|
contextInfo: { stanzaId: content.context.message_id },
|
|
};
|
|
return convertMessage;
|
|
}
|
|
return {
|
|
documentMessage: message,
|
|
};
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
protected async eventHandler(content: any) {
|
|
try {
|
|
// Registro para depuración
|
|
this.logger.log('Contenido recibido en eventHandler:');
|
|
this.logger.log(JSON.stringify(content, null, 2));
|
|
|
|
const database = this.configService.get<Database>('DATABASE');
|
|
const settings = await this.findSettings();
|
|
|
|
// Si hay mensajes, verificar primero el tipo
|
|
if (content.messages && content.messages.length > 0) {
|
|
const message = content.messages[0];
|
|
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
|
|
|
|
// Verificamos el tipo de mensaje antes de procesarlo
|
|
if (
|
|
message.type === 'text' ||
|
|
message.type === 'image' ||
|
|
message.type === 'video' ||
|
|
message.type === 'audio' ||
|
|
message.type === 'document' ||
|
|
message.type === 'sticker' ||
|
|
message.type === 'location' ||
|
|
message.type === 'contacts' ||
|
|
message.type === 'interactive' ||
|
|
message.type === 'button' ||
|
|
message.type === 'reaction'
|
|
) {
|
|
// Procesar el mensaje normalmente
|
|
this.messageHandle(content, database, settings);
|
|
} else {
|
|
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
|
|
}
|
|
} else if (content.statuses) {
|
|
// Procesar actualizaciones de estado
|
|
this.messageHandle(content, database, settings);
|
|
} else {
|
|
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('Error en eventHandler:');
|
|
this.logger.error(error);
|
|
}
|
|
}
|
|
|
|
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
|
try {
|
|
let quoted: any;
|
|
let webhookUrl: any;
|
|
if (options?.quoted) {
|
|
const m = options?.quoted;
|
|
|
|
const msg = m?.key;
|
|
|
|
if (!msg) {
|
|
throw 'Message not found';
|
|
}
|
|
|
|
quoted = msg;
|
|
}
|
|
if (options?.webhookUrl) {
|
|
webhookUrl = options.webhookUrl;
|
|
}
|
|
|
|
let content: any;
|
|
const messageSent = await (async () => {
|
|
if (message['reactionMessage']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: 'reaction',
|
|
to: number.replace(/\D/g, ''),
|
|
reaction: {
|
|
message_id: message['reactionMessage']['key']['id'],
|
|
emoji: message['reactionMessage']['text'],
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['locationMessage']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: 'location',
|
|
to: number.replace(/\D/g, ''),
|
|
location: {
|
|
longitude: message['locationMessage']['degreesLongitude'],
|
|
latitude: message['locationMessage']['degreesLatitude'],
|
|
name: message['locationMessage']['name'],
|
|
address: message['locationMessage']['address'],
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['contacts']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: 'contacts',
|
|
to: number.replace(/\D/g, ''),
|
|
contacts: message['contacts'],
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
message = message['message'];
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['conversation']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: 'text',
|
|
to: number.replace(/\D/g, ''),
|
|
text: {
|
|
body: message['conversation'],
|
|
preview_url: Boolean(options?.linkPreview),
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['media']) {
|
|
const isImage = message['mimetype']?.startsWith('image/');
|
|
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: message['mediaType'],
|
|
to: number.replace(/\D/g, ''),
|
|
[message['mediaType']]: {
|
|
[message['type']]: message['id'],
|
|
...(message['mediaType'] !== 'audio' &&
|
|
message['fileName'] &&
|
|
!isImage && { filename: message['fileName'] }),
|
|
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['audio']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
type: 'audio',
|
|
to: number.replace(/\D/g, ''),
|
|
audio: {
|
|
[message['type']]: message['id'],
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['buttons']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
to: number.replace(/\D/g, ''),
|
|
type: 'interactive',
|
|
interactive: {
|
|
type: 'button',
|
|
body: {
|
|
text: message['text'] || 'Select',
|
|
},
|
|
action: {
|
|
buttons: message['buttons'],
|
|
},
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
let formattedText = '';
|
|
for (const item of message['buttons']) {
|
|
formattedText += `▶️ ${item.reply?.title}\n`;
|
|
}
|
|
message = { conversation: `${message['text'] || 'Select'}\n` + formattedText };
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['listMessage']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
to: number.replace(/\D/g, ''),
|
|
type: 'interactive',
|
|
interactive: {
|
|
type: 'list',
|
|
header: {
|
|
type: 'text',
|
|
text: message['listMessage']['title'],
|
|
},
|
|
body: {
|
|
text: message['listMessage']['description'],
|
|
},
|
|
footer: {
|
|
text: message['listMessage']['footerText'],
|
|
},
|
|
action: {
|
|
button: message['listMessage']['buttonText'],
|
|
sections: message['listMessage']['sections'],
|
|
},
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
let formattedText = '';
|
|
for (const section of message['listMessage']['sections']) {
|
|
formattedText += `${section?.title}\n`;
|
|
for (const row of section.rows) {
|
|
formattedText += `${row?.title}\n`;
|
|
}
|
|
}
|
|
message = { conversation: `${message['listMessage']['title']}\n` + formattedText };
|
|
return await this.post(content, 'messages');
|
|
}
|
|
if (message['template']) {
|
|
content = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
to: number.replace(/\D/g, ''),
|
|
type: 'template',
|
|
template: {
|
|
name: message['template']['name'],
|
|
language: {
|
|
code: message['template']['language'] || 'en_US',
|
|
},
|
|
components: message['template']['components'],
|
|
},
|
|
};
|
|
quoted ? (content.context = { message_id: quoted.id }) : content;
|
|
message = { conversation: `▶️${message['template']['name']}◀️` };
|
|
return await this.post(content, 'messages');
|
|
}
|
|
})();
|
|
|
|
if (messageSent?.error_data || messageSent.message) {
|
|
this.logger.error(messageSent);
|
|
return messageSent;
|
|
}
|
|
|
|
const messageRaw: any = {
|
|
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) },
|
|
message: this.convertMessageToRaw(message, content),
|
|
messageType: this.renderMessageType(content.type),
|
|
messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
|
|
instanceId: this.instanceId,
|
|
webhookUrl,
|
|
status: status[1],
|
|
source: 'unknown',
|
|
};
|
|
|
|
this.logger.log(messageRaw);
|
|
|
|
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.SEND_MESSAGE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
messageRaw,
|
|
);
|
|
}
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration)
|
|
await chatbotController.emit({
|
|
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
|
remoteJid: messageRaw.key.remoteJid,
|
|
msg: messageRaw,
|
|
pushName: messageRaw.pushName,
|
|
});
|
|
|
|
await this.prismaRepository.message.create({
|
|
data: messageRaw,
|
|
});
|
|
|
|
return messageRaw;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
// Send Message Controller
|
|
public async textMessage(data: SendTextDto, isIntegration = false) {
|
|
const res = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
conversation: data.text,
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
return res;
|
|
}
|
|
|
|
private async getIdMedia(mediaMessage: any, isFile = false) {
|
|
try {
|
|
const formData = new FormData();
|
|
|
|
if (isFile === false) {
|
|
if (isURL(mediaMessage.media)) {
|
|
const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
|
|
const buffer = Buffer.from(response.data, 'base64');
|
|
formData.append('file', buffer, {
|
|
filename: mediaMessage.fileName || 'media',
|
|
contentType: mediaMessage.mimetype,
|
|
});
|
|
} else {
|
|
const buffer = Buffer.from(mediaMessage.media, 'base64');
|
|
formData.append('file', buffer, {
|
|
filename: mediaMessage.fileName || 'media',
|
|
contentType: mediaMessage.mimetype,
|
|
});
|
|
}
|
|
} else {
|
|
formData.append('file', mediaMessage.media.buffer, {
|
|
filename: mediaMessage.media.originalname,
|
|
contentType: mediaMessage.media.mimetype,
|
|
});
|
|
}
|
|
|
|
const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
|
|
|
|
formData.append('typeFile', mimetype);
|
|
formData.append('messaging_product', 'whatsapp');
|
|
|
|
const token = this.token;
|
|
|
|
const headers = { Authorization: `Bearer ${token}` };
|
|
const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
|
|
this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
|
|
}/${this.number}/media`;
|
|
|
|
const res = await axios.post(url, formData, { headers });
|
|
return res.data.id;
|
|
} catch (error) {
|
|
this.logger.error(error.response.data);
|
|
throw new InternalServerErrorException(error?.toString() || error);
|
|
}
|
|
}
|
|
|
|
protected async prepareMediaMessage(mediaMessage: MediaMessage) {
|
|
try {
|
|
if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) {
|
|
const regex = new RegExp(/.*\/(.+?)\./);
|
|
const arrayMatch = regex.exec(mediaMessage.media);
|
|
mediaMessage.fileName = arrayMatch[1];
|
|
}
|
|
|
|
if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) {
|
|
mediaMessage.fileName = 'image.png';
|
|
}
|
|
|
|
if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) {
|
|
mediaMessage.fileName = 'video.mp4';
|
|
}
|
|
|
|
let mimetype: string | false;
|
|
|
|
const prepareMedia: any = {
|
|
caption: mediaMessage?.caption,
|
|
fileName: mediaMessage.fileName,
|
|
mediaType: mediaMessage.mediatype,
|
|
media: mediaMessage.media,
|
|
gifPlayback: false,
|
|
};
|
|
|
|
if (isURL(mediaMessage.media)) {
|
|
mimetype = mimeTypes.lookup(mediaMessage.media);
|
|
prepareMedia.id = mediaMessage.media;
|
|
prepareMedia.type = 'link';
|
|
} else {
|
|
mimetype = mimeTypes.lookup(mediaMessage.fileName);
|
|
const id = await this.getIdMedia(prepareMedia);
|
|
prepareMedia.id = id;
|
|
prepareMedia.type = 'id';
|
|
}
|
|
|
|
prepareMedia.mimetype = mimetype;
|
|
|
|
return prepareMedia;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException(error?.toString() || error);
|
|
}
|
|
}
|
|
|
|
public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) {
|
|
const mediaData: SendMediaDto = { ...data };
|
|
|
|
if (file) mediaData.media = file.buffer.toString('base64');
|
|
|
|
const message = await this.prepareMediaMessage(mediaData);
|
|
|
|
const mediaSent = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{ ...message },
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
|
|
return mediaSent;
|
|
}
|
|
|
|
public async processAudio(audio: string, number: string, file: any) {
|
|
number = number.replace(/\D/g, '');
|
|
const hash = `${number}-${new Date().getTime()}`;
|
|
|
|
if (process.env.API_AUDIO_CONVERTER) {
|
|
this.logger.verbose('Using audio converter API');
|
|
const formData = new FormData();
|
|
|
|
if (file) {
|
|
formData.append('file', file.buffer, {
|
|
filename: file.originalname,
|
|
contentType: file.mimetype,
|
|
});
|
|
} else if (isURL(audio)) {
|
|
formData.append('url', audio);
|
|
} else {
|
|
formData.append('base64', audio);
|
|
}
|
|
|
|
formData.append('format', 'mp3');
|
|
|
|
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
|
},
|
|
});
|
|
|
|
const audioConverter = response?.data?.audio || response?.data?.url;
|
|
|
|
if (!audioConverter) {
|
|
throw new InternalServerErrorException('Failed to convert audio');
|
|
}
|
|
|
|
const prepareMedia: any = {
|
|
fileName: `${hash}.mp3`,
|
|
mediaType: 'audio',
|
|
media: audioConverter,
|
|
mimetype: 'audio/mpeg',
|
|
};
|
|
|
|
const id = await this.getIdMedia(prepareMedia);
|
|
prepareMedia.id = id;
|
|
prepareMedia.type = 'id';
|
|
|
|
this.logger.verbose('Audio converted');
|
|
return prepareMedia;
|
|
} else {
|
|
let mimetype: string | false;
|
|
|
|
const prepareMedia: any = {
|
|
fileName: `${hash}.mp3`,
|
|
mediaType: 'audio',
|
|
media: audio,
|
|
};
|
|
|
|
if (isURL(audio)) {
|
|
mimetype = mimeTypes.lookup(audio);
|
|
prepareMedia.id = audio;
|
|
prepareMedia.type = 'link';
|
|
} else if (audio && !file) {
|
|
mimetype = mimeTypes.lookup(prepareMedia.fileName);
|
|
const id = await this.getIdMedia(prepareMedia);
|
|
prepareMedia.id = id;
|
|
prepareMedia.type = 'id';
|
|
} else if (file) {
|
|
prepareMedia.media = file;
|
|
const id = await this.getIdMedia(prepareMedia, true);
|
|
prepareMedia.id = id;
|
|
prepareMedia.type = 'id';
|
|
mimetype = file.mimetype;
|
|
}
|
|
|
|
prepareMedia.mimetype = mimetype;
|
|
|
|
return prepareMedia;
|
|
}
|
|
}
|
|
|
|
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
|
const message = await this.processAudio(data.audio, data.number, file);
|
|
|
|
const audioSent = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{ ...message },
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
|
|
return audioSent;
|
|
}
|
|
|
|
public async buttonMessage(data: SendButtonsDto) {
|
|
const embeddedMedia: any = {};
|
|
|
|
const btnItems = {
|
|
text: data.buttons.map((btn) => btn.displayText),
|
|
ids: data.buttons.map((btn) => btn.id),
|
|
};
|
|
|
|
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
|
|
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
|
|
}
|
|
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
text: !embeddedMedia?.mediaKey ? data.title : undefined,
|
|
buttons: data.buttons.map((button) => {
|
|
return {
|
|
type: 'reply',
|
|
reply: {
|
|
title: button.displayText,
|
|
id: button.id,
|
|
},
|
|
};
|
|
}),
|
|
[embeddedMedia?.mediaKey]: embeddedMedia?.message,
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async locationMessage(data: SendLocationDto) {
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
locationMessage: {
|
|
degreesLatitude: data.latitude,
|
|
degreesLongitude: data.longitude,
|
|
name: data?.name,
|
|
address: data?.address,
|
|
},
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async listMessage(data: SendListDto) {
|
|
const sectionsItems = {
|
|
title: data.sections.map((list) => list.title),
|
|
};
|
|
|
|
if (!arrayUnique(sectionsItems.title)) {
|
|
throw new BadRequestException('Section tiles cannot be repeated');
|
|
}
|
|
|
|
const sendData: any = {
|
|
listMessage: {
|
|
title: data.title,
|
|
description: data.description,
|
|
footerText: data?.footerText,
|
|
buttonText: data?.buttonText,
|
|
sections: data.sections.map((section) => {
|
|
return {
|
|
title: section.title,
|
|
rows: section.rows.map((row) => {
|
|
return {
|
|
title: row.title,
|
|
description: row.description.substring(0, 72),
|
|
id: row.rowId,
|
|
};
|
|
}),
|
|
};
|
|
}),
|
|
},
|
|
};
|
|
|
|
return await this.sendMessageWithTyping(data.number, sendData, {
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
});
|
|
}
|
|
|
|
public async templateMessage(data: SendTemplateDto, isIntegration = false) {
|
|
const res = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
template: {
|
|
name: data.name,
|
|
language: data.language,
|
|
components: data.components,
|
|
},
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
webhookUrl: data?.webhookUrl,
|
|
},
|
|
isIntegration,
|
|
);
|
|
return res;
|
|
}
|
|
|
|
public async contactMessage(data: SendContactDto) {
|
|
const message: any = {};
|
|
|
|
const vcard = (contact: ContactMessage) => {
|
|
let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`;
|
|
|
|
if (contact.organization) {
|
|
result += `ORG:${contact.organization};\n`;
|
|
}
|
|
|
|
if (contact.email) {
|
|
result += `EMAIL:${contact.email}\n`;
|
|
}
|
|
|
|
if (contact.url) {
|
|
result += `URL:${contact.url}\n`;
|
|
}
|
|
|
|
if (!contact.wuid) {
|
|
contact.wuid = createJid(contact.phoneNumber);
|
|
}
|
|
|
|
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';
|
|
|
|
return result;
|
|
};
|
|
|
|
if (data.contact.length === 1) {
|
|
message.contact = {
|
|
displayName: data.contact[0].fullName,
|
|
vcard: vcard(data.contact[0]),
|
|
};
|
|
} else {
|
|
message.contactsArrayMessage = {
|
|
displayName: `${data.contact.length} contacts`,
|
|
contacts: data.contact.map((contact) => {
|
|
return {
|
|
displayName: contact.fullName,
|
|
vcard: vcard(contact),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
contacts: data.contact.map((contact) => {
|
|
return {
|
|
name: { formatted_name: contact.fullName, first_name: contact.fullName },
|
|
phones: [{ phone: contact.phoneNumber }],
|
|
urls: [{ url: contact.url }],
|
|
emails: [{ email: contact.email }],
|
|
org: { company: contact.organization },
|
|
};
|
|
}),
|
|
message,
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async reactionMessage(data: SendReactionDto) {
|
|
return await this.sendMessageWithTyping(data.key.remoteJid, {
|
|
reactionMessage: {
|
|
key: data.key,
|
|
text: data.reaction,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async getBase64FromMediaMessage(data: any) {
|
|
try {
|
|
const msg = data.message;
|
|
const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
|
|
const mediaMessage = msg.message[messageType];
|
|
|
|
return {
|
|
mediaType: msg.messageType,
|
|
fileName: mediaMessage?.fileName,
|
|
caption: mediaMessage?.caption,
|
|
size: {
|
|
fileLength: mediaMessage?.fileLength,
|
|
height: mediaMessage?.fileLength,
|
|
width: mediaMessage?.width,
|
|
},
|
|
mimetype: mediaMessage?.mime_type,
|
|
base64: msg.message.base64,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
public async deleteMessage() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
|
|
// methods not available on WhatsApp Business API
|
|
public async mediaSticker() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async pollMessage() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async statusMessage() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async reloadConnection() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async whatsappNumber() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async markMessageAsRead() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async archiveChat() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async markChatUnread() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fetchProfile() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async offerCall() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async sendPresence() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async setPresence() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fetchPrivacySettings() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updatePrivacySettings() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fetchBusinessProfile() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateProfileName() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateProfileStatus() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateProfilePicture() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async removeProfilePicture() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async blockUser() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateMessage() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async createGroup() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateGroupPicture() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateGroupSubject() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateGroupDescription() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async findGroup() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fetchAllGroups() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async inviteCode() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async inviteInfo() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async sendInvite() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async acceptInviteCode() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async revokeInviteCode() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async findParticipants() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateGParticipant() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async updateGSetting() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async toggleEphemeral() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async leaveGroup() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fetchLabels() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async handleLabel() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async receiveMobileCode() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
public async fakeCall() {
|
|
throw new BadRequestException('Method not available on WhatsApp Business API');
|
|
}
|
|
}
|