From 1686ef58cf3ddf3a97fc9290a1c6a81a35945916 Mon Sep 17 00:00:00 2001 From: microprocessgit Date: Tue, 16 Jan 2024 16:19:30 -0300 Subject: [PATCH] added: Integration with whatsapp business api --- package.json | 2 +- src/config/env.config.ts | 8 + src/config/logger.config.ts | 6 +- src/validate/validate.schema.ts | 49 +- src/whatsapp/controllers/chat.controller.ts | 6 + .../controllers/instance.controller.ts | 33 +- .../controllers/sendMessage.controller.ts | 6 + .../controllers/webhook.controller.ts | 15 +- src/whatsapp/dto/chat.dto.ts | 5 +- src/whatsapp/dto/instance.dto.ts | 1 + src/whatsapp/dto/sendMessage.dto.ts | 9 + src/whatsapp/routers/chat.router.ts | 21 +- src/whatsapp/routers/index.router.ts | 2 +- src/whatsapp/routers/sendMessage.router.ts | 18 + src/whatsapp/routers/webhook.router.ts | 29 +- src/whatsapp/services/chatwoot.service.ts | 72 +- src/whatsapp/services/monitor.service.ts | 40 +- src/whatsapp/services/typebot.service.ts | 91 +- .../services/whatsapp.baileys.service.ts | 2813 ++++++++++++++++ .../services/whatsapp.business.service.ts | 1098 +++++++ src/whatsapp/services/whatsapp.service.ts | 2924 +---------------- src/whatsapp/types/wa.types.ts | 9 + src/whatsapp/whatsapp.module.ts | 20 +- 23 files changed, 4406 insertions(+), 2871 deletions(-) create mode 100644 src/whatsapp/services/whatsapp.baileys.service.ts create mode 100644 src/whatsapp/services/whatsapp.business.service.ts diff --git a/package.json b/package.json index cbf580f2..e177bc0e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", "@sentry/node": "^7.59.2", - "@whiskeysockets/baileys": "^6.5.0", + "@whiskeysockets/baileys": "github:PurpShell/Baileys#combined", "amqplib": "^0.10.3", "aws-sdk": "^2.1499.0", "axios": "^1.3.5", diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 6b48a0a0..cdd2e46a 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -120,6 +120,7 @@ export type EventsWebhook = { export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; +export type WABussiness = { ACESS_TOKEN: string, URL: string, VERSION: string, LANGUAGE: string }; export type Auth = { API_KEY: ApiKey; @@ -161,6 +162,7 @@ export interface Env { TYPEBOT: Typebot; AUTHENTICATION: Auth; PRODUCTION?: Production; + WABUSSINESS: WABussiness; } export type Key = keyof Env; @@ -337,6 +339,12 @@ export class ConfigService { SECRET: process.env.AUTHENTICATION_JWT_SECRET || 'L=0YWt]b2w[WF>#>:&E`', }, }, + WABUSSINESS: { + ACESS_TOKEN: process.env.ACESS_TOKEN, + URL: process.env.URL, + VERSION: process.env.VERSION, + LANGUAGE: process.env.LANGUAGE + }, }; } } diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index c96fa788..93065d2a 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -155,7 +155,7 @@ export class Logger { function salvarLog(env: any, log: string): void { mkdir(env.LOG_PATH, { recursive: true }, (err) => { if (err) throw err; }); - let file = new Date().toLocaleDateString().replaceAll('/', ''); + let file = new Date().toLocaleDateString('pt-BR').replaceAll('/', ''); file = env.LOG_PATH + '/' + file + '.txt'; try { if (fs.existsSync(file)) { @@ -176,9 +176,9 @@ function excluirArquivosAntigos(path: string, diasLimite: number): void { data.setDate(data.getDate() - diasLimite); fs.readdirSync(path).forEach((nomeArquivo) => { let Timerfile = new Date(parseInt(nomeArquivo.substring(8, 4)), - parseInt(nomeArquivo.substring(4, 2)), parseInt(nomeArquivo.substring(2, 0))) + 1-parseInt(nomeArquivo.substring(4, 2)), parseInt(nomeArquivo.substring(2, 0))) - if (Timerfile.getTime() < data.getTime()) { + if ((Timerfile.getTime() > 0) && (Timerfile.getTime() < data.getTime())) { fs.unlinkSync(path + '/' + nomeArquivo); console.log(`Arquivo ${nomeArquivo} excluído.`); } diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 4b803873..0f1eb582 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -5,10 +5,10 @@ const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { const properties = {}; propertyNames.forEach( (property) => - (properties[property] = { - minLength: 1, - description: `The "${property}" cannot be empty`, - }), + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), ); return { if: { @@ -389,6 +389,25 @@ export const listMessageSchema: JSONSchema7 = { required: ['number', 'listMessage'], }; +export const templateMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + templateMessage: { + type: 'object', + properties: { + name: { type: 'string' }, + language: { type: 'string' }, + }, + required: ['name', 'language'], + ...isNotEmpty('name', 'language'), + }, + }, + required: ['templateMessage', 'number'], +}; + export const contactMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', @@ -601,6 +620,28 @@ export const profilePictureSchema: JSONSchema7 = { }, }; + +export const profileBusinessSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + about: { type: 'string' }, + address: { type: 'string' }, + description: { type: 'string' }, + vertical: { type: 'string' }, + email: { type: 'string' }, + profile_picture_handle: { type: 'string' }, + websites: { + type: 'array', + minItems: 1, + items: { + type: 'string', + }, + }, + }, +}; + export const profileSchema: JSONSchema7 = { type: 'object', properties: { diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 60a9c618..4d97256b 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -11,6 +11,7 @@ import { ReadMessageDto, SendPresenceDto, WhatsAppNumberDto, + NumberBusiness, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -117,4 +118,9 @@ export class ChatController { logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); } + + public async setWhatsappBusinessProfile({ instanceName }: InstanceDto, data: NumberBusiness) { + logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].setWhatsappBusinessProfile(data); + } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0f06895e..a6bed9ba 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -3,7 +3,7 @@ import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import { v4 } from 'uuid'; -import { ConfigService, HttpServer } from '../../config/env.config'; +import { ConfigService, HttpServer, WABussiness } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { RedisCache } from '../../libs/redis.client'; @@ -19,8 +19,8 @@ import { SqsService } from '../services/sqs.service'; import { TypebotService } from '../services/typebot.service'; import { WebhookService } from '../services/webhook.service'; import { WebsocketService } from '../services/websocket.service'; -import { WAStartupService } from '../services/whatsapp.service'; -import { Events, wa } from '../types/wa.types'; +import { Events, wa, Integration } from '../types/wa.types'; +import { WAStartupClass } from '../whatsapp.module'; export class InstanceController { constructor( @@ -50,6 +50,7 @@ export class InstanceController { events, qrcode, number, + integration, token, chatwoot_account_id, chatwoot_token, @@ -85,9 +86,10 @@ export class InstanceController { await this.authService.checkDuplicateToken(token); this.logger.verbose('creating instance'); - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + const instance = new WAStartupClass[integration](this.configService, this.eventEmitter, this.repository, this.cache); instance.instanceName = instanceName; - + instance.instanceNumber = number; + instance.instanceToken = token; const instanceId = v4(); instance.sendDataWebhook(Events.INSTANCE_CREATE, { @@ -359,20 +361,30 @@ export class InstanceController { this.settingsService.create(instance, settings); + const urlServer = this.configService.get('SERVER').URL; + let webhook_url = '', acess_token = ''; + if(integration === Integration.WHATSAPP_BUSINESS) + { + webhook_url = `${urlServer}/webhook/whatsapp/${encodeURIComponent(instance.instanceName)}`; + acess_token = this.configService.get('WABUSSINESS').ACESS_TOKEN; + } + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { let getQrcode: wa.QrCode; + + await this.waMonitor.saveInstance({integration, instanceName, token, number}); if (qrcode) { this.logger.verbose('creating qrcode'); await instance.connectToWhatsapp(number); await delay(5000); getQrcode = instance.qrCode; } - const result = { instance: { instanceName: instance.instanceName, instanceId: instanceId, + integration: integration, status: 'created', }, hash, @@ -405,6 +417,7 @@ export class InstanceController { listening_from_me: typebot_listening_from_me, }, settings, + webhook_url: webhook_url, qrcode: getQrcode, proxy, }; @@ -423,6 +436,10 @@ export class InstanceController { throw new BadRequestException('token is required'); } + if (!integration) { + throw new BadRequestException('integration is required'); + } + if (!chatwoot_url) { throw new BadRequestException('url is required'); } @@ -443,8 +460,6 @@ export class InstanceController { throw new BadRequestException('conversation_pending is required'); } - const urlServer = this.configService.get('SERVER').URL; - try { this.chatwootService.create(instance, { enabled: true, @@ -617,7 +632,7 @@ export class InstanceController { await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); this.logger.verbose('close connection instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + this.waMonitor.waInstances[instanceName]?.closeClient(); return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } }; } catch (error) { diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index 20e38ae5..858a7219 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -15,6 +15,7 @@ import { SendStatusDto, SendStickerDto, SendTextDto, + SendTemplateDto, } from '../dto/sendMessage.dto'; import { WAMonitoringService } from '../services/monitor.service'; @@ -86,6 +87,11 @@ export class SendMessageController { return await this.waMonitor.waInstances[instanceName].listMessage(data); } + public async sendTemplate({ instanceName }: InstanceDto, data: SendTemplateDto) { + logger.verbose('requested sendList from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].templateMessage(data); + } + public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { logger.verbose('requested sendContact from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].contactMessage(data); diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index 8201f1b5..6f251b38 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -5,11 +5,19 @@ import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { WebhookService } from '../services/webhook.service'; +import { ConfigService } from '../../config/env.config'; +import { RedisCache } from '../../libs/redis.client'; +import EventEmitter2 from 'eventemitter2'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('WebhookController'); export class WebhookController { - constructor(private readonly webhookService: WebhookService) {} + constructor( + private readonly webhookService: WebhookService, + private readonly waMonitor: WAMonitoringService, + ) {} public async createWebhook(instance: InstanceDto, data: WebhookDto) { logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); @@ -61,4 +69,9 @@ export class WebhookController { logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); return this.webhookService.find(instance); } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].connectToWhatsapp(data); + } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 07553c90..49396500 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -26,8 +26,11 @@ export class NumberBusiness { message?: string; description?: string; email?: string; - website?: string[]; + websites?: string[]; address?: string; + about?: string; + vertical?: string; + profilehandle?: string; } export class ProfileNameDto { diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 2bf5c362..ed78da15 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -3,6 +3,7 @@ export class InstanceDto { instanceId?: string; qrcode?: boolean; number?: string; + integration?: string; token?: string; webhook?: string; webhook_by_events?: boolean; diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index bfa5763f..cd299764 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -134,6 +134,15 @@ export class SendListDto extends Metadata { listMessage: ListMessage; } +export class TemplateMessage { + name: string; + language: string; +} + +export class SendTemplateDto extends Metadata { + templateMessage: TemplateMessage; +} + export class ContactMessage { fullName: string; wuid: string; diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 29d1cdc3..b1583a89 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -15,6 +15,7 @@ import { profileStatusSchema, readMessageSchema, whatsappNumberSchema, + profileBusinessSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { @@ -29,6 +30,7 @@ import { ReadMessageDto, SendPresenceDto, WhatsAppNumberDto, + NumberBusiness, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -213,6 +215,23 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .post(this.routerPath('setWhatsappBusinessProfile'), ...guards, async (req, res) => { + logger.verbose('request received in findStatusMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: messageUpSchema, + ClassRef: NumberBusiness, + execute: (instance, data) => chatController.setWhatsappBusinessProfile(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) .get(this.routerPath('findChats'), ...guards, async (req, res) => { logger.verbose('request received in findChats'); logger.verbose('request body: '); @@ -291,7 +310,7 @@ export class ChatRouter extends RouterBroker { const response = await this.dataValidate({ request: req, - schema: profilePictureSchema, + schema: profileBusinessSchema, ClassRef: ProfilePictureDto, execute: (instance, data) => chatController.fetchBusinessProfile(instance, data), }); diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 56b9301f..77214d46 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -53,7 +53,7 @@ router .use('/message', new MessageRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) - .use('/webhook', new WebhookRouter(...guards).router) + .use('/webhook', new WebhookRouter(configService, ...guards).router) .use('/chatwoot', new ChatwootRouter(...guards).router) .use('/settings', new SettingsRouter(...guards).router) .use('/websocket', new WebsocketRouter(...guards).router) diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index d87db44d..de8798e1 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -13,6 +13,7 @@ import { statusMessageSchema, stickerMessageSchema, textMessageSchema, + templateMessageSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { @@ -27,6 +28,7 @@ import { SendStatusDto, SendStickerDto, SendTextDto, + SendTemplateDto, } from '../dto/sendMessage.dto'; import { sendMessageController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -133,6 +135,22 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('sendTemplate'), ...guards, async (req, res) => { + logger.verbose('request received in sendTemplate'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: templateMessageSchema, + ClassRef: SendTemplateDto, + execute: (instance, data) => sendMessageController.sendTemplate(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) .post(this.routerPath('sendContact'), ...guards, async (req, res) => { logger.verbose('request received in sendContact'); logger.verbose('request body: '); diff --git a/src/whatsapp/routers/webhook.router.ts b/src/whatsapp/routers/webhook.router.ts index 835d6014..620866af 100644 --- a/src/whatsapp/routers/webhook.router.ts +++ b/src/whatsapp/routers/webhook.router.ts @@ -7,11 +7,12 @@ import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { webhookController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; +import { WABussiness, ConfigService } from '../../config/env.config'; const logger = new Logger('WebhookRouter'); export class WebhookRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { + constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { super(); this.router .post(this.routerPath('set'), ...guards, async (req, res) => { @@ -30,6 +31,32 @@ export class WebhookRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('whatsapp'), async (req, res) => { + logger.verbose('request received in findChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance, data) => webhookController.receiveWebhook(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('whatsapp'), async (req, res) => { + logger.verbose('request received in webhook'); + logger.verbose('request query: '); + logger.verbose(req.query); + if (req.query['hub.verify_token'] === this.configService.get('WABUSSINESS').ACESS_TOKEN) + res.send(req.query['hub.challenge']); + else + res.send('Error, wrong validation token'); + logger.verbose('Error, wrong validation token'); + }) .get(this.routerPath('find'), ...guards, async (req, res) => { logger.verbose('request received in findWebhook'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 26b0cce9..e3563302 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -6,12 +6,12 @@ import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; -import { ConfigService, HttpServer } from '../../config/env.config'; +import { ConfigService, HttpServer, WABussiness } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { ROOT_DIR } from '../../config/path.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; -import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto, SendTemplateDto } from '../dto/sendMessage.dto'; import { MessageRaw } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events } from '../types/wa.types'; @@ -457,7 +457,7 @@ export class ChatwootService { if (!findParticipant.name || findParticipant.name === chatId) { await this.updateContact(instance, findParticipant.id, { name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, + avatar_url: picture_url?.profilePictureUrl || null, }); } } else { @@ -467,7 +467,7 @@ export class ChatwootService { filterInbox.id, false, body.pushName, - picture_url.profilePictureUrl || null, + picture_url?.profilePictureUrl || null, body.key.participant, ); } @@ -483,7 +483,7 @@ export class ChatwootService { if (body.key.fromMe) { if (findContact) { contact = await this.updateContact(instance, findContact.id, { - avatar_url: picture_url.profilePictureUrl || null, + avatar_url: picture_url?.profilePictureUrl || null, }); } else { const jid = isGroup ? null : body.key.remoteJid; @@ -493,7 +493,7 @@ export class ChatwootService { filterInbox.id, isGroup, nameContact, - picture_url.profilePictureUrl || null, + picture_url?.profilePictureUrl || null, jid, ); } @@ -502,11 +502,11 @@ export class ChatwootService { if (!findContact.name || findContact.name === chatId) { contact = await this.updateContact(instance, findContact.id, { name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, + avatar_url: picture_url?.profilePictureUrl || null, }); } else { contact = await this.updateContact(instance, findContact.id, { - avatar_url: picture_url.profilePictureUrl || null, + avatar_url: picture_url?.profilePictureUrl || null, }); } if (!contact) { @@ -520,7 +520,7 @@ export class ChatwootService { filterInbox.id, isGroup, nameContact, - picture_url.profilePictureUrl || null, + picture_url?.profilePictureUrl || null, jid, ); } @@ -785,9 +785,9 @@ export class ChatwootService { const replyToIds = await this.getReplyToIds(messageBody, instance); if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { - data.append('content_attributes', { - ...replyToIds, - }); + data.append('content_attributes', + JSON.stringify(replyToIds), + ); } } @@ -1023,10 +1023,10 @@ export class ChatwootService { // Chatwoot to Whatsapp const messageReceived = body.content ? body.content - .replaceAll(/(?('WABUSSINESS').LANGUAGE, + }, + options: { + delay: 1200, + presence: 'composing', + quoted: await this.getQuotedMessage(body, instance), + }, + }; + + const messageSent = await waInstance?.templateMessage(data, true); + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + { + messageId: body.id, + inboxId: body.inbox?.id, + conversationId: body.conversation?.id, + }, + instance, + ); + return + } if (senderName === null || senderName === undefined) { formatText = messageReceived; } else { @@ -1137,7 +1167,7 @@ export class ChatwootService { for (const message of body.conversation.messages) { this.logger.verbose('check if message is media'); - if (message.attachments && message.attachments.length > 0) { + if (message?.attachments && message?.attachments.length > 0) { this.logger.verbose('message is media'); for (const attachment of message.attachments) { this.logger.verbose('send media to whatsapp'); @@ -1513,9 +1543,9 @@ export class ChatwootService { const originalMessage = await this.getConversationMessage(body.message); const bodyMessage = originalMessage ? originalMessage - .replaceAll(/\*((?!\s)([^\n*]+?)(?('LOG').BAILEYS; + public stateConnection: wa.StateConnection = { state: 'close' }; + + public async getProfileName() { + this.logger.verbose('Getting profile name'); + + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; + if (!profileName) { + this.logger.verbose('Profile name not found, trying to get from database'); + if (this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('Database enabled, trying to get from database'); + const collection = dbserver + .getClient() + .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(this.instanceName); + const data = await collection.findOne({ _id: 'creds' }); + if (data) { + this.logger.verbose('Profile name found in database'); + const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { + this.logger.verbose('Profile name found in file'); + const creds = JSON.parse( + readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { + encoding: 'utf-8', + }), + ); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } + + this.logger.verbose(`Profile name: ${profileName}`); + return profileName; + } + + public async getProfileStatus() { + this.logger.verbose('Getting profile status'); + const status = await this.client.fetchStatus(this.instance.wuid); + + this.logger.verbose(`Profile status: ${status.status}`); + return status.status; + } + + public async setSettings(data: SettingsRaw) { + await super.setSettings(data); + this.client?.ws?.close(); + } + + public async setProxy(data: ProxyRaw, reload = true) { + await super.setProxy(data); + if (reload) { + this.reloadConnection(); + } + } + + protected async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + this.logger.verbose('Connection update'); + if (qr) { + this.logger.verbose('QR code found'); + if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + this.logger.verbose('QR code limit reached'); + + this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); + } + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + state: 'refused', + statusReason: DisconnectReason.connectionClosed, + }); + + this.logger.verbose('endSession defined as true'); + this.endSession = true; + + this.logger.verbose('Emmiting event logout.instance'); + return this.eventEmitter.emit('no.connection', this.instance.name); + } + + this.logger.verbose('Incrementing QR code count'); + this.instance.qrcode.count++; + + const color = this.configService.get('QRCODE').COLOR; + + const optsQrcode: QRCodeToDataURLOptions = { + margin: 3, + scale: 4, + errorCorrectionLevel: 'H', + color: { light: '#ffffff', dark: color }, + }; + + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); + } else { + this.instance.qrcode.pairingCode = null; + } + + this.logger.verbose('Generating QR code'); + qrcode.toDataURL(qr, optsQrcode, (error, base64) => { + if (error) { + this.logger.error('Qrcode generate failed:' + error.toString()); + return; + } + + this.instance.qrcode.base64 = base64; + this.instance.qrcode.code = qr; + + this.sendDataWebhook(Events.QRCODE_UPDATED, { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }, + ); + } + }); + + this.logger.verbose('Generating QR code in terminal'); + qrcodeTerminal.generate(qr, { small: true }, (qrcode) => + this.logger.log( + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + qrcode, + ), + ); + } + + if (connection) { + this.logger.verbose('Connection found'); + this.stateConnection = { + state: connection, + statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + }; + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); + } + + if (connection === 'close') { + this.logger.verbose('Connection closed'); + const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + this.logger.verbose('Reconnecting to whatsapp'); + await this.connectToWhatsapp(); + } else { + this.logger.verbose('Do not reconnect to whatsapp'); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'closed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'closed', + }, + ); + } + + this.logger.verbose('Emittin event logout.instance'); + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.client?.ws?.close(); + this.client.end(new Error('Close connection')); + this.logger.verbose('Connection closed'); + } + } + + if (connection === 'open') { + this.logger.verbose('Connection opened'); + this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); + this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; + const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' '); + const formattedName = this.instance.name; + this.logger.info( + ` + ┌──────────────────────────────┐ + │ CONNECTED TO WHATSAPP │ + └──────────────────────────────┘`.replace(/^ +/gm, ' '), + ); + this.logger.info(` + wuid: ${formattedWuid} + name: ${formattedName} + `); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } + } + } + + protected async getMessage(key: proto.IMessageKey, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + try { + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as proto.IWebMessageInfo[]; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; + } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; + + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); + + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } + } + + protected async defineAuthState() { + const db = this.configService.get('DATABASE'); + await super.defineAuthState(); + this.logger.verbose('Store file enabled'); + + if (db.SAVE_DATA.INSTANCE && db.ENABLED) { + this.logger.verbose('Database enabled'); + return await useMultiFileAuthStateDb(this.instance.name); + } + + return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + } + + public async connectToWhatsapp(number?: string): Promise { + await super.connectToWhatsapp(); + try { + this.instance.authState = await this.defineAuthState(); + const { version } = await fetchLatestBaileysVersion(); + this.logger.verbose('Baileys version: ' + version); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + this.logger.verbose('Browser: ' + JSON.stringify(browser)); + + let options; + + if (this.localProxy.enabled) { + this.logger.info('Proxy enabled: ' + this.localProxy.proxy); + + if (this.localProxy.proxy.includes('proxyscrape')) { + const response = await axios.get(this.localProxy.proxy); + const text = response.data; + const proxyUrls = text.split('\r\n'); + const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); + const proxyUrl = 'http://' + proxyUrls[rand]; + options = { + agent: new ProxyAgent(proxyUrl as any), + }; + } else { + options = { + agent: new ProxyAgent(this.localProxy.proxy as any), + }; + } + } + + const socketConfig: UserFacingSocketConfig = { + ...options, + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser, + version, + markOnlineOnConnect: this.localSettings.always_online, + retryRequestDelayMs: 10, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + shouldIgnoreJid: (jid) => { + const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); + const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); + + return isGroupJid || isBroadcast; + }, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: false, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; + }, + }; + + this.endSession = false; + + this.logger.verbose('Creating socket'); + + this.client = makeWASocket(socketConfig); + + this.logger.verbose('Socket created'); + + this.eventHandler(); + + this.logger.verbose('Socket event handler initialized'); + + this.phoneNumber = number; + + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } + } + + public async reloadConnection(): Promise { + try { + this.instance.authState = await this.defineAuthState(); + + const { version } = await fetchLatestBaileysVersion(); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + + let options; + + if (this.localProxy.enabled) { + this.logger.verbose('Proxy enabled'); + options = { + agent: new ProxyAgent(this.localProxy.proxy as any), + fetchAgent: new ProxyAgent(this.localProxy.proxy as any), + }; + } + + const socketConfig: UserFacingSocketConfig = { + ...options, + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser, + version, + markOnlineOnConnect: this.localSettings.always_online, + retryRequestDelayMs: 10, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + shouldIgnoreJid: (jid) => { + const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); + const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); + + return isGroupJid || isBroadcast; + }, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: false, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; + }, + }; + + this.client = makeWASocket(socketConfig); + + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } + } + + protected readonly chatHandle = { + 'chats.upsert': async (chats: Chat[], database: Database) => { + this.logger.verbose('Event received: chats.upsert'); + + this.logger.verbose('Finding chats in database'); + const chatsRepository = await this.repository.chat.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if chats exists in database to insert'); + const chatsRaw: ChatRaw[] = []; + for await (const chat of chats) { + if (chatsRepository.find((cr) => cr.id === chat.id)) { + continue; + } + + chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); + } + + this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); + this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + }, + + 'chats.update': async ( + chats: Partial< + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { + conditional: (bufferedData: BufferedEventData) => boolean; + } + >[], + ) => { + this.logger.verbose('Event received: chats.update'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { id: chat.id, owner: this.instance.wuid }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); + this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + }, + + 'chats.delete': async (chats: string[]) => { + this.logger.verbose('Event received: chats.delete'); + + this.logger.verbose('Deleting chats in database'); + chats.forEach( + async (chat) => + await this.repository.chat.delete({ + where: { owner: this.instance.name, id: chat }, + }), + ); + + this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); + this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + }, + }; + + protected readonly contactHandle = { + 'contacts.upsert': async (contacts: Contact[], database: Database) => { + this.logger.verbose('Event received: contacts.upsert'); + + this.logger.verbose('Finding contacts in database'); + const contactsRepository = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if contacts exists in database to insert'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + if (contactsRepository.find((cr) => cr.id === contact.id)) { + continue; + } + + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Inserting contacts in database'); + this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + + 'contacts.update': async (contacts: Partial[], database: Database) => { + this.logger.verbose('Event received: contacts.update'); + + this.logger.verbose('Verifying if contacts exists in database to update'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name ?? contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + }; + + protected readonly messageHandle = { + 'messaging-history.set': async ( + { + messages, + chats, + isLatest, + }: { + chats: Chat[]; + contacts: Contact[]; + messages: proto.IWebMessageInfo[]; + isLatest: boolean; + }, + database: Database, + ) => { + this.logger.verbose('Event received: messaging-history.set'); + if (isLatest) { + this.logger.verbose('isLatest defined as true'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { + id: chat.id, + owner: this.instance.name, + lastMsgTimestamp: chat.lastMessageRecvTimestamp, + }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_SET'); + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + } + + const messagesRaw: MessageRaw[] = []; + const messagesRepository = await this.repository.message.find({ + where: { owner: this.instance.name }, + }); + for await (const [, m] of Object.entries(messages)) { + if (!m.message) { + continue; + } + if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { + continue; + } + + if (Long.isLong(m?.messageTimestamp)) { + m.messageTimestamp = m.messageTimestamp?.toNumber(); + } + + messagesRaw.push({ + key: m.key, + pushName: m.pushName, + participant: m.participant, + message: { ...m.message }, + messageType: getContentType(m.message), + messageTimestamp: m.messageTimestamp as number, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); + + messages = undefined; + }, + + 'messages.upsert': async ( + { + messages, + type, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + }, + database: Database, + settings: SettingsRaw, + ) => { + try { + this.logger.verbose('Event received: messages.upsert'); + for (const received of messages) { + if ( + (type !== 'notify' && type !== 'append') || + received.message?.protocolMessage || + received.message?.pollUpdateMessage + ) { + this.logger.verbose('message rejected'); + return; + } + + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); + } + + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + + let messageRaw: MessageRaw; + + if ( + (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || + received?.message?.imageMessage + ) { + const buffer = await downloadMediaMessage( + { key: received.key, message: received?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); + messageRaw = { + key: received.key, + pushName: received.pushName, + message: { + ...received.message, + base64: buffer ? buffer.toString('base64') : undefined, + }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + } else { + messageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + } + + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled && !received.key.id.includes('@broadcast')) { + const chatwootSentMessage = await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + + if (chatwootSentMessage?.id) { + messageRaw.chatwoot = { + messageId: chatwootSentMessage.id, + inboxId: chatwootSentMessage.inbox_id, + conversationId: chatwootSentMessage.conversation_id, + }; + } + } + + const typebotSessionRemoteJid = this.localTypebot.sessions?.find( + (session) => session.remoteJid === received.key.remoteJid, + ); + + if ((this.localTypebot.enabled && type === 'notify') || typebotSessionRemoteJid) { + if (!(this.localTypebot.listening_from_me === false && messageRaw.key.fromMe === true)) { + if (messageRaw.messageType !== 'reactionMessage') + await this.typebotService.sendTypebot( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + } + + if (this.localChamaai.enabled && messageRaw.key.fromMe === false && type === 'notify') { + await this.chamaaiService.sendChamaai( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + } + } catch (error) { + this.logger.error(error); + } + }, + + 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { + this.logger.verbose('Event received: messages.update'); + const status: Record = { + 0: 'ERROR', + 1: 'PENDING', + 2: 'SERVER_ACK', + 3: 'DELIVERY_ACK', + 4: 'READ', + 5: 'PLAYED', + }; + for await (const { key, update } of args) { + if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + let pollUpdates: any; + if (update.pollUpdates) { + this.logger.verbose('Poll update found'); + + this.logger.verbose('Getting poll message'); + const pollCreation = await this.getMessage(key); + this.logger.verbose(pollCreation); + + if (pollCreation) { + this.logger.verbose('Getting aggregate votes in poll message'); + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } + } + + if (status[update.status] === 'READ' && !key.fromMe) return; + + if (update.message === null && update.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.MESSAGES_DELETE, + { instanceName: this.instance.name }, + { key: key }, + ); + } + + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: status[update.status], + datetime: Date.now(), + owner: this.instance.name, + pollUpdates, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); + } + } + }, + }; + + protected readonly groupHandler = { + 'groups.upsert': (groupMetadata: GroupMetadata[]) => { + this.logger.verbose('Event received: groups.upsert'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); + this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); + }, + + 'groups.update': (groupMetadataUpdate: Partial[]) => { + this.logger.verbose('Event received: groups.update'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); + this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); + }, + + 'group-participants.update': (participantsUpdate: { + id: string; + participants: string[]; + action: ParticipantAction; + }) => { + this.logger.verbose('Event received: group-participants.update'); + + this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); + }, + }; + + protected eventHandler() { + this.logger.verbose('Initializing event handler'); + this.client.ev.process(async (events) => { + if (!this.endSession) { + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); + + if (events.call) { + this.logger.verbose('Listening event: call'); + const call = events.call[0]; + + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } + + if (settings?.msg_call?.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + const msg = await this.client.sendMessage(call.from, { + text: settings.msg_call, + }); + + this.logger.verbose('Sending data to event messages.upsert'); + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); + } + + this.logger.verbose('Sending data to webhook in event CALL'); + this.sendDataWebhook(Events.CALL, call); + } + + if (events['connection.update']) { + this.logger.verbose('Listening event: connection.update'); + this.connectionUpdate(events['connection.update']); + } + + if (events['creds.update']) { + this.logger.verbose('Listening event: creds.update'); + this.instance.authState.saveCreds(); + } + + if (events['messaging-history.set']) { + this.logger.verbose('Listening event: messaging-history.set'); + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload, database); + } + + if (events['messages.upsert']) { + this.logger.verbose('Listening event: messages.upsert'); + const payload = events['messages.upsert']; + if (payload.messages.find((a) => a?.messageStubType === 2)) { + const msg = payload.messages[0]; + retryCache[msg.key.id] = msg; + return; + } + this.messageHandle['messages.upsert'](payload, database, settings); + } + + if (events['messages.update']) { + this.logger.verbose('Listening event: messages.update'); + const payload = events['messages.update']; + payload.forEach((message) => { + if (retryCache[message.key.id]) { + this.client.ev.emit('messages.upsert', { + messages: [message], + type: 'notify', + }); + delete retryCache[message.key.id]; + return; + } + }); + this.messageHandle['messages.update'](payload, database, settings); + } + + if (events['presence.update']) { + this.logger.verbose('Listening event: presence.update'); + const payload = events['presence.update']; + + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } + + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } + + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } + + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } + + if (events['chats.upsert']) { + this.logger.verbose('Listening event: chats.upsert'); + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload, database); + } + + if (events['chats.update']) { + this.logger.verbose('Listening event: chats.update'); + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } + + if (events['chats.delete']) { + this.logger.verbose('Listening event: chats.delete'); + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } + + if (events['contacts.upsert']) { + this.logger.verbose('Listening event: contacts.upsert'); + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload, database); + } + + if (events['contacts.update']) { + this.logger.verbose('Listening event: contacts.update'); + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload, database); + } + } + }); + } + + // Check if the number is MX or AR + protected formatMXOrARNumber(jid: string): string { + const countryCode = jid.substring(0, 2); + + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + if (jid.length === 13) { + const number = countryCode + jid.substring(3); + return number; + } + + return jid; + } + return jid; + } + + // Check if the number is br + protected formatBRNumber(jid: string) { + const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); + if (regexp.test(jid)) { + const match = regexp.exec(jid); + if (match && match[1] === '55') { + const joker = Number.parseInt(match[3][0]); + const ddd = Number.parseInt(match[2]); + if (joker < 7 || ddd < 31) { + return match[0]; + } + return match[1] + match[2] + match[3]; + } + return jid; + } else { + return jid; + } + } + + protected createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); + + if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net or @lid'); + return number; + } + + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; + } + + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(':')[0] + .split('@')[0]; + + if (number.includes('-') && number.length >= 24) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + number = number.replace(/\D/g, ''); + + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } + + public async profilePicture(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile picture with jid: ' + jid); + try { + this.logger.verbose('Getting profile picture url'); + return { + wuid: jid, + profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), + }; + } catch (error) { + this.logger.verbose('Profile picture not found'); + return { + wuid: jid, + profilePictureUrl: null, + }; + } + } + + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(info?.jid); + const status = await this.getStatus(info?.jid); + const business = await this.fetchBusinessProfile(info?.jid); + + return { + wuid: info?.jid || jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.websites?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + const business = await this.fetchBusinessProfile(jid); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.websites?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + + protected async sendMessageWithTyping( + number: string, + message: T, + options?: Options, + isChatwoot = false, + ) { + this.logger.verbose('Sending message with typing'); + + this.logger.verbose(`Check if number "${number}" is WhatsApp`); + const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); + + this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + try { + if (options?.delay) { + this.logger.verbose('Delaying message'); + + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); + + await delay(options.delay); + this.logger.verbose('Set delay: ' + options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } + + const linkPreview = options?.linkPreview != false ? undefined : false; + + let quoted: WAMessage; + + if (options?.quoted) { + const m = options?.quoted; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let mentions: string[]; + if (isJidGroup(sender)) { + try { + const group = await this.findGroup({ groupJid: sender }, 'inner'); + + if (!group) { + throw new NotFoundException('Group not found'); + } + + if (options?.mentions) { + this.logger.verbose('Mentions defined'); + + if (options.mentions?.everyOne) { + this.logger.verbose('Mentions everyone'); + + this.logger.verbose('Getting group metadata'); + mentions = group.participants.map((participant) => participant.id); + this.logger.verbose('Getting group metadata for mentions'); + } else if (options.mentions?.mentioned?.length) { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + return null; + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); + } + } + + const messageSent = await (async () => { + const option = { + quoted, + }; + + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + !message['conversation'] && + sender !== 'status@broadcast' + ) { + if (message['reactionMessage']) { + this.logger.verbose('Sending reaction'); + return await this.client.sendMessage( + sender, + { + react: { + text: message['reactionMessage']['text'], + key: message['reactionMessage']['key'], + }, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + + if (!message['audio']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, + }, + mentions, + }, + option as unknown as MiscMessageGenerationOptions, + ); + } + } + if (message['conversation']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + text: message['conversation'], + mentions, + linkPreview: linkPreview, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + + if (sender === 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, + { + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); + } + + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + })(); + + const messageRaw: MessageRaw = { + key: messageSent.key, + pushName: messageSent.pushName, + message: { ...messageSent.message }, + messageType: getContentType(messageSent.message), + messageTimestamp: messageSent.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + if (this.localChatwoot.enabled && !isChatwoot) { + this.chatwootService.eventWhatsapp(Events.SEND_MESSAGE, { instanceName: this.instance.name }, messageRaw); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + return messageSent; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Instance Controller + public async sendPresence(data: SendPresenceDto) { + try { + const { number } = data; + + this.logger.verbose(`Check if number "${number}" is WhatsApp`); + const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); + + this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + this.logger.verbose('Sending presence'); + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing'); + + await delay(data.options.delay); + this.logger.verbose('Set delay: ' + data.options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Send Message Controller + public async textMessage(data: SendTextDto, isChatwoot = false) { + this.logger.verbose('Sending text message'); + return await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, + }, + data?.options, + isChatwoot, + ); + } + + public async pollMessage(data: SendPollDto) { + this.logger.verbose('Sending poll message'); + return await this.sendMessageWithTyping( + data.number, + { + poll: { + name: data.pollMessage.name, + selectableCount: data.pollMessage.selectableCount, + values: data.pollMessage.values, + }, + }, + data?.options, + ); + } + + protected async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); + } + + if (!status.content) { + throw new BadRequestException('Content is required'); + } + + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); + + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); + } + + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); + } + + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); + + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } + + if (!status.font) { + throw new BadRequestException('Font is required'); + } + + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); + + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); + + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); + + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); + + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; + + fs.unlinkSync(convert); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + const status = await this.formatStatusMessage(data.statusMessage); + + return await this.sendMessageWithTyping('status@broadcast', { + status, + }); + } + + protected async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + const prepareMedia = await prepareWAMessageMedia( + { + [mediaMessage.mediatype]: isURL(mediaMessage.media) + ? { url: mediaMessage.media } + : Buffer.from(mediaMessage.media, 'base64'), + } as any, + { upload: this.client.waUploadToServer }, + ); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose('If media type is document and file name is not defined then'); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { + mediaMessage.fileName = 'image.png'; + } + + if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { + mediaMessage.fileName = 'video.mp4'; + } + + let mimetype: string; + + if (mediaMessage.mimetype) { + mimetype = mediaMessage.mimetype; + } else { + if (isURL(mediaMessage.media)) { + const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' }); + + mimetype = response.headers['content-type']; + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } + } + + this.logger.verbose('Mimetype: ' + mimetype); + + prepareMedia[mediaType].caption = mediaMessage?.caption; + prepareMedia[mediaType].mimetype = mimetype; + prepareMedia[mediaType].fileName = mediaMessage.fileName; + + if (mediaMessage.mediatype === 'video') { + this.logger.verbose('Is media type video then set gif playback as false'); + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), + ); + prepareMedia[mediaType].gifPlayback = false; + } + + this.logger.verbose('Generating wa message from content'); + return generateWAMessageFromContent( + '', + { [mediaType]: { ...prepareMedia[mediaType] } }, + { userJid: this.instance.wuid }, + ); + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + protected async convertToWebP(image: string, number: string) { + try { + this.logger.verbose('Converting image to WebP to sticker'); + + let imagePath: string; + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to image name: ' + hash); + + const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; + this.logger.verbose('Output path: ' + outputPath); + + if (isBase64(image)) { + this.logger.verbose('Image is base64'); + + const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const imageBuffer = Buffer.from(base64Data, 'base64'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } else { + this.logger.verbose('Image is url'); + + const timestamp = new Date().getTime(); + const url = `${image}?timestamp=${timestamp}`; + this.logger.verbose('including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting image from url'); + + const imageBuffer = Buffer.from(response.data, 'binary'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } + + await sharp(imagePath).webp().toFile(outputPath); + this.logger.verbose('Image converted to WebP'); + + fs.unlinkSync(imagePath); + this.logger.verbose('Temp image deleted'); + + return outputPath; + } catch (error) { + console.error('Erro ao converter a imagem para WebP:', error); + } + } + + public async mediaSticker(data: SendStickerDto) { + this.logger.verbose('Sending media sticker'); + const convert = await this.convertToWebP(data.stickerMessage.image, data.number); + const result = await this.sendMessageWithTyping( + data.number, + { + sticker: { url: convert }, + }, + data?.options, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted image deleted'); + + return result; + } + + public async mediaMessage(data: SendMediaDto, isChatwoot = false) { + this.logger.verbose('Sending media message'); + const generate = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options, isChatwoot); + } + + public async processAudio(audio: string, number: string) { + this.logger.verbose('Processing audio'); + let tempAudioPath: string; + let outputAudio: string; + + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to audio name: ' + hash); + + if (isURL(audio)) { + this.logger.verbose('Audio is url'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const timestamp = new Date().getTime(); + const url = `${audio}?timestamp=${timestamp}`; + + this.logger.verbose('Including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting audio from url'); + + fs.writeFileSync(tempAudioPath, response.data); + } else { + this.logger.verbose('Audio is base64'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const audioBuffer = Buffer.from(audio, 'base64'); + fs.writeFileSync(tempAudioPath, audioBuffer); + this.logger.verbose('Temp audio created'); + } + + this.logger.verbose('Converting audio to mp4'); + return new Promise((resolve, reject) => { + exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { + fs.unlinkSync(tempAudioPath); + this.logger.verbose('Temp audio deleted'); + + if (error) reject(error); + + this.logger.verbose('Audio converted to mp4'); + resolve(outputAudio); + }); + }); + } + + public async audioWhatsapp(data: SendAudioDto, isChatwoot = false) { + this.logger.verbose('Sending audio whatsapp'); + + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; + } + + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, + isChatwoot, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + isChatwoot, + ); + } + + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; + + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + } + + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; + + 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, + { + buttonsMessage: { + text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, + contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, + footerText: data.buttonMessage?.footerText, + buttons: data.buttonMessage.buttons.map((button) => { + return { + buttonText: { + displayText: button.buttonText, + }, + buttonId: button.buttonId, + type: 1, + }; + }), + headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + }, + data?.options, + ); + } + + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } + + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + return await this.sendMessageWithTyping( + data.number, + { + listMessage: { + title: data.listMessage.title, + description: data.listMessage.description, + buttonText: data.listMessage?.buttonText, + footerText: data.listMessage?.footerText, + sections: data.listMessage.sections, + listType: 1, + }, + }, + data?.options, + ); + } + + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + const message: proto.IMessage = {}; + + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } + + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + // Chat Controller + public async whatsappNumber(data: WhatsAppNumberDto) { + this.logger.verbose('Getting whatsapp number'); + + const onWhatsapp: OnWhatsAppDto[] = []; + for await (const number of data.numbers) { + let jid = this.createJid(number); + + if (isJidGroup(jid)) { + const group = await this.findGroup({ groupJid: jid }, 'inner'); + + if (!group) throw new BadRequestException('Group not found'); + + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else { + jid = !jid.startsWith('+') ? `+${jid}` : jid; + const verify = await this.client.onWhatsApp(jid); + + const result = verify[0]; + + if (!result) { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); + } else { + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); + } + } + } + + return onWhatsapp; + } + + public async markMessageAsRead(data: ReadMessageDto) { + this.logger.verbose('Marking message as read'); + + try { + const keys: proto.IMessageKey[] = []; + data.read_messages.forEach((read) => { + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); + } + }); + await this.client.readMessages(keys); + return { message: 'Read messages', read: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Read messages fail', error.toString()); + } + } + + public async getLastMessage(number: string) { + const messages = await this.fetchMessages({ + where: { + key: { + remoteJid: number, + }, + owner: this.instance.name, + }, + }); + + let lastMessage = messages.pop(); + + for (const message of messages) { + if (message.messageTimestamp >= lastMessage.messageTimestamp) { + lastMessage = message; + } + } + + return lastMessage as unknown as LastMessage; + } + + public async archiveChat(data: ArchiveChatDto) { + this.logger.verbose('Archiving chat'); + try { + let last_message = data.lastMessage; + let number = data.chat; + + if (!last_message && number) { + last_message = await this.getLastMessage(number); + } else { + last_message = data.lastMessage; + last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); + number = last_message?.key?.remoteJid; + } + + if (!last_message || Object.keys(last_message).length === 0) { + throw new NotFoundException('Last message not found'); + } + + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [last_message], + }, + this.createJid(number), + ); + + return { + chatId: number, + archived: true, + }; + } catch (error) { + throw new InternalServerErrorException({ + archived: false, + message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], + }); + } + } + + public async deleteMessage(del: DeleteMessage) { + this.logger.verbose('Deleting message'); + try { + return await this.client.sendMessage(del.remoteJid, { delete: del }); + } catch (error) { + throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); + } + } + + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { + this.logger.verbose('Getting base64 from media message'); + try { + const m = data?.message; + const convertToMp4 = data?.convertToMp4 ?? false; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + for (const subtype of MessageSubtype) { + if (msg.message[subtype]) { + msg.message = msg.message[subtype].message; + } + } + + let mediaMessage: any; + let mediaType: string; + + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; + } + } + + if (!mediaMessage) { + throw 'The message is not of the media type'; + } + + if (typeof mediaMessage['mediaKey'] === 'object') { + msg.message = JSON.parse(JSON.stringify(msg.message)); + } + + this.logger.verbose('Downloading media message'); + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); + const typeMessage = getContentType(msg.message); + + if (convertToMp4 && typeMessage === 'audioMessage') { + this.logger.verbose('Converting audio to mp4'); + const number = msg.key.remoteJid.split('@')[0]; + const convert = await this.processAudio(buffer.toString('base64'), number); + + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + this.logger.verbose('Audio converted to mp4'); + + const result = { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: 'audio/mp4', + base64: Buffer.from(audio, 'base64').toString('base64'), + }; + + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); + + this.logger.verbose('Media message downloaded'); + return result; + } + } + + this.logger.verbose('Media message downloaded'); + return { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: mediaMessage['mimetype'], + base64: buffer.toString('base64'), + }; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + public async fetchContacts(query: ContactQuery) { + this.logger.verbose('Fetching contacts'); + if (query?.where) { + query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } + } else { + query = { + where: { + owner: this.instance.name, + }, + }; + } + return await this.repository.contact.find(query); + } + + public async fetchMessages(query: MessageQuery) { + this.logger.verbose('Fetching messages'); + if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.message.find(query); + } + + public async fetchStatusMessage(query: MessageUpQuery) { + this.logger.verbose('Fetching status messages'); + if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.messageUpdate.find(query); + } + + public async fetchChats() { + this.logger.verbose('Fetching chats'); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); + } + + public async fetchPrivacySettings() { + this.logger.verbose('Fetching privacy settings'); + const privacy = await this.client.fetchPrivacySettings(); + + return { + readreceipts: privacy.readreceipts, + profile: privacy.profile, + status: privacy.status, + online: privacy.online, + last: privacy.last, + groupadd: privacy.groupadd, + }; + } + + public async updatePrivacySettings(settings: PrivacySettingDto) { + this.logger.verbose('Updating privacy settings'); + try { + await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); + this.logger.verbose('Read receipts privacy updated'); + + await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); + this.logger.verbose('Profile picture privacy updated'); + + await this.client.updateStatusPrivacy(settings.privacySettings.status); + this.logger.verbose('Status privacy updated'); + + await this.client.updateOnlinePrivacy(settings.privacySettings.online); + this.logger.verbose('Online privacy updated'); + + await this.client.updateLastSeenPrivacy(settings.privacySettings.last); + this.logger.verbose('Last seen privacy updated'); + + await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); + this.logger.verbose('Groups add privacy updated'); + + this.reloadConnection(); + + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating privacy settings', error.toString()); + } + } + + public async fetchBusinessProfile(number: string): Promise { + this.logger.verbose('Fetching business profile'); + try { + const jid = number ? this.createJid(number) : this.instance.wuid; + + const profile = await this.client.getBusinessProfile(jid); + this.logger.verbose('Trying to get business profile'); + + if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; + } + + this.logger.verbose('Business profile fetched'); + return { + isBusiness: true, + ...profile, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileName(name: string) { + this.logger.verbose('Updating profile name to ' + name); + try { + await this.client.updateProfileName(name); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileStatus(status: string) { + this.logger.verbose('Updating profile status to: ' + status); + try { + await this.client.updateProfileStatus(status); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile status', error.toString()); + } + } + + public async updateProfilePicture(picture: string) { + this.logger.verbose('Updating profile picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + + await this.client.updateProfilePicture(this.instance.wuid, pic); + this.logger.verbose('Profile picture updated'); + + this.reloadConnection(); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile picture', error.toString()); + } + } + + public async removeProfilePicture() { + this.logger.verbose('Removing profile picture'); + try { + await this.client.removeProfilePicture(this.instance.wuid); + + this.reloadConnection(); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error removing profile picture', error.toString()); + } + } + + // Group + public async createGroup(create: CreateGroupDto) { + this.logger.verbose('Creating group: ' + create.subject); + try { + const participants = (await this.whatsappNumber({ numbers: create.participants })) + .filter((participant) => participant.exists) + .map((participant) => participant.jid); + const { id } = await this.client.groupCreate(create.subject, participants); + this.logger.verbose('Group created: ' + id); + + if (create?.description) { + this.logger.verbose('Updating group description: ' + create.description); + await this.client.groupUpdateDescription(id, create.description); + } + + if (create?.promoteParticipants) { + this.logger.verbose('Prometing group participants: ' + participants); + await this.updateGParticipant({ + groupJid: id, + action: 'promote', + participants: participants, + }); + } + + this.logger.verbose('Getting group metadata'); + const group = await this.client.groupMetadata(id); + + return group; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException('Error creating group', error.toString()); + } + } + + public async updateGroupPicture(picture: GroupPictureDto) { + this.logger.verbose('Updating group picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture.image)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture.image}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture.image)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture.image, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(picture.groupJid, pic); + this.logger.verbose('Group picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error update group picture', error.toString()); + } + } + + public async updateGroupSubject(data: GroupSubjectDto) { + this.logger.verbose('Updating group subject to: ' + data.subject); + try { + await this.client.groupUpdateSubject(data.groupJid, data.subject); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group subject', error.toString()); + } + } + + public async updateGroupDescription(data: GroupDescriptionDto) { + this.logger.verbose('Updating group description to: ' + data.description); + try { + await this.client.groupUpdateDescription(data.groupJid, data.description); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group description', error.toString()); + } + } + + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { + this.logger.verbose('Fetching group'); + try { + return await this.client.groupMetadata(id.groupJid); + } catch (error) { + if (reply === 'inner') { + return; + } + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async fetchAllGroups(getParticipants: GetParticipant) { + this.logger.verbose('Fetching all groups'); + try { + const fetch = Object.values(await this.client.groupFetchAllParticipating()); + + const groups = fetch.map((group) => { + const result = { + id: group.id, + subject: group.subject, + subjectOwner: group.subjectOwner, + subjectTime: group.subjectTime, + size: group.participants.length, + creation: group.creation, + owner: group.owner, + desc: group.desc, + descId: group.descId, + restrict: group.restrict, + announce: group.announce, + }; + + if (getParticipants.getParticipants == 'true') { + result['participants'] = group.participants; + } + + return result; + }); + + return groups; + } catch (error) { + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async inviteCode(id: GroupJid) { + this.logger.verbose('Fetching invite code for group: ' + id.groupJid); + try { + const code = await this.client.groupInviteCode(id.groupJid); + return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + } catch (error) { + throw new NotFoundException('No invite code', error.toString()); + } + } + + public async closeClient() { + this.client?.ws?.close(); + } + + public async inviteInfo(id: GroupInvite) { + this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); + try { + return await this.client.groupGetInviteInfo(id.inviteCode); + } catch (error) { + throw new NotFoundException('No invite info', id.inviteCode); + } + } + + public async sendInvite(id: GroupSendInvite) { + this.logger.verbose('Sending invite for group: ' + id.groupJid); + try { + const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); + this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); + + const inviteUrl = inviteCode.inviteUrl; + this.logger.verbose('Invite url: ' + inviteUrl); + + const numbers = id.numbers.map((number) => this.createJid(number)); + const description = id.description ?? ''; + + const msg = `${description}\n\n${inviteUrl}`; + + const message = { + conversation: msg, + }; + + for await (const number of numbers) { + await this.sendMessageWithTyping(number, message); + } + + this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); + + return { send: true, inviteUrl }; + } catch (error) { + throw new NotFoundException('No send invite'); + } + } + + public async revokeInviteCode(id: GroupJid) { + this.logger.verbose('Revoking invite code for group: ' + id.groupJid); + try { + const inviteCode = await this.client.groupRevokeInvite(id.groupJid); + return { revoked: true, inviteCode }; + } catch (error) { + throw new NotFoundException('Revoke error', error.toString()); + } + } + + public async findParticipants(id: GroupJid) { + this.logger.verbose('Fetching participants for group: ' + id.groupJid); + try { + const participants = (await this.client.groupMetadata(id.groupJid)).participants; + return { participants }; + } catch (error) { + throw new NotFoundException('No participants', error.toString()); + } + } + + public async updateGParticipant(update: GroupUpdateParticipantDto) { + this.logger.verbose('Updating participants'); + try { + const participants = update.participants.map((p) => this.createJid(p)); + const updateParticipants = await this.client.groupParticipantsUpdate( + update.groupJid, + participants, + update.action, + ); + return { updateParticipants: updateParticipants }; + } catch (error) { + throw new BadRequestException('Error updating participants', error.toString()); + } + } + + public async updateGSetting(update: GroupUpdateSettingDto) { + this.logger.verbose('Updating setting for group: ' + update.groupJid); + try { + const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); + return { updateSetting: updateSetting }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async toggleEphemeral(update: GroupToggleEphemeralDto) { + this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); + try { + await this.client.groupToggleEphemeral(update.groupJid, update.expiration); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async leaveGroup(id: GroupJid) { + this.logger.verbose('Leaving group: ' + id.groupJid); + try { + await this.client.groupLeave(id.groupJid); + return { groupJid: id.groupJid, leave: true }; + } catch (error) { + throw new BadRequestException('Unable to leave the group', error.toString()); + } + } +} diff --git a/src/whatsapp/services/whatsapp.business.service.ts b/src/whatsapp/services/whatsapp.business.service.ts new file mode 100644 index 00000000..0738c25d --- /dev/null +++ b/src/whatsapp/services/whatsapp.business.service.ts @@ -0,0 +1,1098 @@ +import axios from 'axios'; +import { arrayUnique, isURL } from 'class-validator'; +import EventEmitter2 from 'eventemitter2'; +import { mkdir } from 'fs'; +import fs from 'fs/promises'; +import { getMIMEType } from 'node-mime-types'; +import { join } from 'path'; +import { ProxyAgent } from 'proxy-agent'; + +import { + ConfigService, + Database, + WABussiness, +} from '../../config/env.config'; +import { INSTANCE_DIR } from '../../config/path.config'; +import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; +import { RedisCache } from '../../libs/redis.client'; + +import { + ContactMessage, + MediaMessage, + Options, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendReactionDto, + SendTextDto, + SendTemplateDto, +} from '../dto/sendMessage.dto'; +import { SettingsRaw } from '../models';; +import { ContactRaw } from '../models/contact.model'; +import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { Events, wa } from '../types/wa.types'; +import { WAStartupService } from './whatsapp.service'; +import { NumberBusiness } from '../dto/chat.dto'; + +const retryCache = {}; + +export class WABussinessService extends WAStartupService { + constructor( + protected readonly configService: ConfigService, + protected readonly eventEmitter: EventEmitter2, + protected readonly repository: RepositoryBroker, + protected readonly cache: RedisCache, + ) { + super(configService, eventEmitter, repository, cache); + this.logger.verbose('WABussinessService initialized'); + } + + public stateConnection = { state: 'open' }; + + public async closeClient() { + this.stateConnection = { state: 'close' }; + } + + protected async defineAuthState(): Promise { + await super.defineAuthState(); + this.logger.verbose('Store file enabled'); + mkdir(join(INSTANCE_DIR, this.instance.name), { recursive: true }, (err) => { if (err) throw err; }); + } + + public async setWhatsappBusinessProfile(data: NumberBusiness): Promise { + this.logger.verbose('set profile'); + let 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'); + } + + protected async getMessage(key: any, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + try { + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as any; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; + } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; + + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); + + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } + } + + public async connectToWhatsapp(data: any): Promise { + await super.connectToWhatsapp(); + if (!data) return + const content = data.entry[0].changes[0].value; + try { + let options; + + if (this.localProxy.enabled) { + this.logger.info('Proxy enabled: ' + this.localProxy.proxy); + + if (this.localProxy.proxy.includes('proxyscrape')) { + const response = await axios.get(this.localProxy.proxy); + const text = response.data; + const proxyUrls = text.split('\r\n'); + const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); + const proxyUrl = 'http://' + proxyUrls[rand]; + options = { + agent: new ProxyAgent(proxyUrl as any), + }; + } else { + options = { + agent: new ProxyAgent(this.localProxy.proxy as any), + }; + } + } + + this.endSession = false; + + this.logger.verbose('Creating socket'); + + + this.logger.verbose('Socket created'); + + this.eventHandler(content); + + this.logger.verbose('Socket event handler initialized'); + + this.phoneNumber = + this.createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id); + + // return this.client; + } 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('WABUSSINESS').URL; + let version = this.configService.get('WABUSSINESS').VERSION; + urlServer = `${urlServer}/${version}/${id}`; + const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.instance.token}` }; + let result = await axios.get(urlServer, { headers }); + result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' }); + return result.data + } catch (e) { + this.logger.error(e); + } + } + + protected async messageHandle(received: any, database: Database, settings: SettingsRaw) { + try { + let messageRaw: MessageRaw; + let pushName: any; + + if (received.contacts) + pushName = received.contacts[0].profile.name + + if (received.messages) { + const key = { + id: received.messages[0].id, + remoteJid: this.phoneNumber, fromMe: received.messages[0].from === received.metadata.phone_number_id + } + if (received?.messages[0].document || received?.messages[0].image + || received?.messages[0].audio || received?.messages[0].video + ) { + const buffer = await this.downloadMediaMessage(received?.messages[0]); + messageRaw = { + key, + pushName, + message: { + ...this.messageMediaJson(received), + base64: buffer ? buffer.toString('base64') : undefined, + }, + messageType: received.messages[0].type, + messageTimestamp: received.messages[0].timestamp as number, + owner: this.instance.name, + // source: getDevice(received.key.id), + }; + } else if (received?.messages[0].interactive) { + messageRaw = { + key, + pushName, + message: { + ...this.messageInteractiveJson(received), + }, + messageType: 'text', + messageTimestamp: received.messages[0].timestamp as number, + owner: this.instance.name, + // source: getDevice(received.key.id), + }; + + } else if (received?.messages[0].contacts) { + messageRaw = { + key, + pushName, + message: { + ...this.messageContactsJson(received), + }, + messageType: 'text', + messageTimestamp: received.messages[0].timestamp as number, + owner: this.instance.name, + // source: getDevice(received.key.id), + }; + + } else { + messageRaw = { + key, + pushName, + message: this.messageTextJson(received), + messageType: received.messages[0].type, + messageTimestamp: received.messages[0].timestamp as number, + owner: this.instance.name, + //source: getDevice(received.key.id), + }; + } + + + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled) { + const chatwootSentMessage = await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + + if (chatwootSentMessage?.id) { + messageRaw.chatwoot = { + messageId: chatwootSentMessage.id, + inboxId: chatwootSentMessage.inbox_id, + conversationId: chatwootSentMessage.conversation_id, + }; + } + } + + const typebotSessionRemoteJid = this.localTypebot.sessions?.find( + (session) => session.remoteJid === key.remoteJid, + ); + + if ((this.localTypebot.enabled) || typebotSessionRemoteJid) { + if (!(this.localTypebot.listening_from_me === false && key.fromMe === true)) { + if (messageRaw.messageType !== 'reactionMessage') + await this.typebotService.sendTypebot( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + } + + if (this.localChamaai.enabled && messageRaw.key.fromMe === false && received?.message.type === 'notify') { + await this.chamaaiService.sendChamaai( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.contacts[0].profile.phone, + pushName, + //profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.contacts[0].profile.phone, + pushName, + //profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + } + this.logger.verbose('Event received: messages.update'); + 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')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + + if (item.status === 'read' && !key.fromMe) return; + + if (item.message === null && item.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.MESSAGES_DELETE, + { instanceName: this.instance.name }, + { key: key }, + ); + } + + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: item.status.toUpperCase(), + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); + } + } + } + } catch (error) { + this.logger.error(error); + } + }; + + protected async eventHandler(content: any) { + this.logger.verbose('Initializing event handler'); + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); + + this.logger.verbose('Listening event: messages.statuses'); + this.messageHandle(content, database, settings); + } + + protected createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); + + if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net or @lid'); + return number; + } + + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; + } + + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(':')[0] + .split('@')[0]; + + if (number.includes('-') && number.length >= 24) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + number = number.replace(/\D/g, ''); + + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } + + protected async sendMessageWithTyping( + number: string, + message: any, + options?: Options, + isChatwoot = false, + ) { + this.logger.verbose('Sending message with typing'); + try { + let quoted: any; + const linkPreview = options?.linkPreview != false ? undefined : false; + if (options?.quoted) { + + const m = options?.quoted; + + const msg = m?.key; + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let content: any; + const messageSent = await (async () => { + if (message['reactionMessage']) { + this.logger.verbose('Sending reaction'); + 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'] + }, + context: { message_id: quoted.id } + } + quoted ? content.context = { message_id: quoted.id } : content + return await this.post(content, 'messages'); + } + if (message['locationMessage']) { + this.logger.verbose('Sending message'); + 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']) { + this.logger.verbose('Sending message'); + 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']) { + this.logger.verbose('Sending message'); + content = { + messaging_product: "whatsapp", + recipient_type: "individual", + type: "text", + to: number.replace(/\D/g, ""), + text: { + body: message['conversation'], + preview_url: linkPreview, + } + } + quoted ? content.context = { message_id: quoted.id } : content + return await this.post(content, 'messages'); + } + if (message['media']) { + this.logger.verbose('Sending message'); + content = { + messaging_product: "whatsapp", + recipient_type: "individual", + type: message['mediaType'], + to: number.replace(/\D/g, ""), + [message['mediaType']]: { + [message['type']]: message['id'], + preview_url: linkPreview, + } + } + quoted ? content.context = { message_id: quoted.id } : content; + return await this.post(content, 'messages'); + } + if (message['buttons']) { + this.logger.verbose('Sending message'); + 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['sections']) { + this.logger.verbose('Sending message'); + content = { + messaging_product: "whatsapp", + recipient_type: "individual", + to: number.replace(/\D/g, ""), + type: 'interactive', + interactive: { + type: "list", + header: { + type: "text", + text: message['title'] + }, + body: { + text: message['text'], + }, + footer: { + text: message['footerText'], + }, + action: { + button: message['buttonText'], + sections: message['sections'], + }, + } + } + quoted ? content.context = { message_id: quoted.id } : content; + let formattedText = ''; + for (const section of message['sections']) { + formattedText += `${section?.title}\n`; + for (const row of section.rows) { + formattedText += `${row?.title}\n`; + } + } + message = { conversation: `${message['title']}\n` + formattedText } + return await this.post(content, 'messages'); + } + if (message['template']) { + this.logger.verbose('Sending message'); + 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" + } + } + } + quoted ? content.context = { message_id: quoted.id } : content; + message = {conversation: `▶️${message['template']['name']}◀️`} + return await this.post(content, 'messages'); + } + } + )(); + + const messageRaw: MessageRaw = { + key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: this.createJid(number) }, + //pushName: messageSent.pushName, + message, + messageType: content.type, + messageTimestamp: messageSent?.messages[0]?.timestamp as number || Math.round(new Date().getTime()/1000), + owner: this.instance.name, + //ource: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + if (this.localChatwoot.enabled && !isChatwoot) { + this.chatwootService.eventWhatsapp(Events.SEND_MESSAGE, { instanceName: this.instance.name }, messageRaw); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + + return messageRaw; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + + private async post(message: any, params: string) { + try{ + let urlServer = this.configService.get('WABUSSINESS').URL; + let version = this.configService.get('WABUSSINESS').VERSION; + urlServer = `${urlServer}/${version}/${this.instance.number}/${params}`; + const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.instance.token}` }; + const result = await axios.post(urlServer, message, { headers }); + return result.data + }catch(e){ + this.logger.error(e); + return e.response.data; + } + + } + + // Send Message Controller + public async textMessage(data: SendTextDto, isChatwoot = false) { + this.logger.verbose('Sending text message'); + const res = await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, + }, + data?.options, + isChatwoot, + ); + return res; + } + + + private async getIdMedia(mediaMessage: any) { + const formData = new FormData(); + let res: any; + const arquivoBuffer = await fs.readFile(mediaMessage.media); + const arquivoBlob = new Blob([arquivoBuffer], { type: mediaMessage.mimetype }) + formData.append('file', arquivoBlob); + formData.append('typeFile', mediaMessage.mimetype); + formData.append('messaging_product', 'whatsapp'); + const headers = { 'Authorization': `Bearer ${this.instance.token}` } + res = await axios.post(process.env.API_URL + '/' + process.env.VERSION + '/' + this.instance.number + '/media', + formData, { headers }); + return res.id; + } + + protected async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose('If media type is document and file name is not defined then'); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { + mediaMessage.fileName = 'image.png'; + } + + if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { + mediaMessage.fileName = 'video.mp4'; + } + + let mimetype: string; + + let prepareMedia: any; + + prepareMedia = + { + caption: mediaMessage?.caption, + fileName: mediaMessage.fileName, + mediaType: mediaMessage.mediatype, + media: mediaMessage.media, + gifPlayback: false + }; + + if (mediaMessage.mimetype) { + mimetype = mediaMessage.mimetype; + } else { + if (isURL(mediaMessage.media)) { + prepareMedia.id = mediaMessage.media + prepareMedia.type = 'link' + } else { + mimetype = getMIMEType(mediaMessage.fileName); + const id = await this.getIdMedia(prepareMedia) + prepareMedia.id = id; + prepareMedia.type = 'id' + } + } + + prepareMedia.mimetype = mimetype; + + this.logger.verbose('Generating wa message from content'); + return prepareMedia; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + + + public async mediaMessage(data: SendMediaDto, isChatwoot = false) { + this.logger.verbose('Sending media message'); + const message = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options, isChatwoot); + } + + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; + + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + } + + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; + + 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.buttonMessage.title : undefined, + buttons: data.buttonMessage.buttons.map((button) => { + return { + type: 'reply', + reply: { + title: button.buttonText, + id: button.buttonId, + } + }; + }), + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + data?.options, + ); + } + + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } + + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + const sectionsItems = { + title: data.listMessage.sections.map((list) => list.title) + }; + + if (!arrayUnique(sectionsItems.title)) { + throw new BadRequestException('Section tiles cannot be repeated'); + } + + return await this.sendMessageWithTyping( + data.number, + { + title: data.listMessage.title, + text: data.listMessage.description, + footerText: data.listMessage?.footerText, + buttonText: data.listMessage?.buttonText, + sections: data.listMessage.sections.map((section) => { + return { + title: section.title, + rows: section.rows.map((row) => { + return { + title: row.title, + description: row.description, + id: row.rowId, + }; + }), + }; + }), + }, + data?.options, + ); + } + + public async templateMessage(data: SendTemplateDto, isChatwoot = false) { + this.logger.verbose('Sending text message'); + const res = await this.sendMessageWithTyping( + data.number, + { + template: { + name: data.templateMessage.name, + language: data.templateMessage.language + } + }, + data?.options, + isChatwoot, + ); + return res; + } + + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + let message: any = {}; + + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } + + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + return await this.sendMessageWithTyping(data.number, { + contacts: data.contactMessage.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 + }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + public async getBase64FromMediaMessage(data: any) { + try { + const msg = data.message; + this.logger.verbose('Getting base64 from media message'); + const messageType = msg.messageType + 'Message'; + let mediaMessage: any; + mediaMessage = msg.message[messageType] + + this.logger.verbose('Media message downloaded'); + 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()); + } + } + + private messageTextJson(received: any) { + let content: any; + const message = received.messages[0] + if (message.from === received.metadata.phone_number_id) { + content = { extendedTextMessage: { text: message.text.body } } + message.context ? + content.extendedTextMessage.contextInfo = { stanzaId: message.context.id } : content + } else { + content = { conversation: message.text.body }; + message.context ? + content.extendedTextMessage = { contextInfo: { stanzaId: message.context.id } } : content + } + return content; + } + + private messageMediaJson(received: any) { + const message = received.messages[0] + let content: any = message.type + 'Message'; + content = { [content]: message[message.type] } + message.context ? + content.extendedTextMessage = { contextInfo: { stanzaId: message.context.id } } : content + return content; + } + + private messageInteractiveJson(received: any) { + const message = received.messages[0] + let content: any = { conversation: message.interactive[message.interactive.type].title }; + message.context ? + content.extendedTextMessage = { contextInfo: { stanzaId: message.context.id } } : content + return content; + } + + private messageContactsJson(received: any) { + const message = received.messages[0]; + let content: any = {}; + + const vcard = (contact: any) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.name.formatted_name}\n` + `FN:${contact.name.formatted_name}\n`; + + if (contact.org) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.org.company};\n`; + } + + if (contact.emails) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.emails[0].email}\n`; + } + + if (contact.urls) { + this.logger.verbose('Url defined'); + result += `URL:${contact.urls[0].url}\n`; + } + + if (!contact.phones[0]?.wa_id) { + this.logger.verbose('Wuid defined'); + contact.phones[0].wa_id = this.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'; + + this.logger.verbose('Vcard created'); + 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.extendedTextMessage = { contextInfo: { stanzaId: message.context.id } } : content + return content; + } + + +} diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3eeb8fbb..6a590835 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,105 +1,23 @@ -import ffmpegPath from '@ffmpeg-installer/ffmpeg'; -import { Boom } from '@hapi/boom'; -import makeWASocket, { - AnyMessageContent, - BufferedEventData, - BufferJSON, - CacheStore, - Chat, - ConnectionState, - Contact, - delay, - DisconnectReason, - downloadMediaMessage, - fetchLatestBaileysVersion, - generateWAMessageFromContent, - getAggregateVotesInPollMessage, - getContentType, - getDevice, - GroupMetadata, - isJidBroadcast, - isJidGroup, - isJidUser, - makeCacheableSignalKeyStore, - MessageUpsertType, - MiscMessageGenerationOptions, - ParticipantAction, - prepareWAMessageMedia, - proto, - useMultiFileAuthState, - UserFacingSocketConfig, - WABrowserDescription, - WAMediaUpload, - WAMessage, - WAMessageUpdate, - WASocket, -} from '@whiskeysockets/baileys'; import axios from 'axios'; -import { exec, execSync } from 'child_process'; -import { arrayUnique, isBase64, isURL } from 'class-validator'; +import { execSync } from 'child_process'; +import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; -import fs, { existsSync, readFileSync } from 'fs'; import Long from 'long'; -import NodeCache from 'node-cache'; -import { getMIMEType } from 'node-mime-types'; -import { release } from 'os'; import { join } from 'path'; -import P from 'pino'; -import { ProxyAgent } from 'proxy-agent'; -import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; -import qrcodeTerminal from 'qrcode-terminal'; -import sharp from 'sharp'; import { v4 } from 'uuid'; import { Auth, CleanStoreConf, ConfigService, - ConfigSessionPhone, Database, HttpServer, Log, - QrCode, - Redis, Sqs, Webhook, Websocket, + Redis, } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; -import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; -import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; -import { getAMQP, removeQueues } from '../../libs/amqp.server'; -import { dbserver } from '../../libs/db.connect'; -import { RedisCache } from '../../libs/redis.client'; -import { getIO } from '../../libs/socket.server'; -import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; -import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; -import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; -import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - LastMessage, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - SendPresenceDto, - WhatsAppNumberDto, -} from '../dto/chat.dto'; -import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, -} from '../dto/group.dto'; import { ContactMessage, MediaMessage, @@ -115,68 +33,137 @@ import { SendStatusDto, SendStickerDto, SendTextDto, + SendTemplateDto, StatusMessage, } from '../dto/sendMessage.dto'; +import { + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, +} from '../dto/group.dto'; +import {getBase64FromMediaMessageDto} from '../dto/chat.dto'; +import { Logger } from '../../config/logger.config'; +import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; +import { InternalServerErrorException, NotFoundException } from '../../exceptions'; +import { getAMQP, removeQueues } from '../../libs/amqp.server'; +import { RedisCache } from '../../libs/redis.client'; +import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; +import { getIO } from '../../libs/socket.server'; +import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { ChamaaiRaw, ProxyRaw, RabbitmqRaw, SettingsRaw, SqsRaw, TypebotRaw } from '../models'; -import { ChatRaw } from '../models/chat.model'; import { ChatwootRaw } from '../models/chatwoot.model'; -import { ContactRaw } from '../models/contact.model'; -import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; import { WebhookRaw } from '../models/webhook.model'; import { WebsocketRaw } from '../models/websocket.model'; -import { ContactQuery } from '../repository/contact.repository'; -import { MessageQuery } from '../repository/message.repository'; -import { MessageUpQuery } from '../repository/messageUp.repository'; import { RepositoryBroker } from '../repository/repository.manager'; -import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; +import { Events, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; import { ChamaaiService } from './chamaai.service'; import { ChatwootService } from './chatwoot.service'; import { TypebotService } from './typebot.service'; +import { ContactQuery } from '../repository/contact.repository'; +import { MessageQuery } from '../repository/message.repository'; +import { MessageUpQuery } from '../repository/messageUp.repository'; -const retryCache = {}; export class WAStartupService { constructor( - private readonly configService: ConfigService, - private readonly eventEmitter: EventEmitter2, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, + protected readonly configService: ConfigService, + protected readonly eventEmitter: EventEmitter2, + protected readonly repository: RepositoryBroker, + protected readonly cache: RedisCache, ) { this.logger.verbose('WAStartupService initialized'); this.cleanStore(); this.cleanDB(); - this.instance.qrcode = { count: 0 }; } - private readonly logger = new Logger(WAStartupService.name); + public client: any; + protected readonly logger = new Logger(WAStartupService.name); public readonly instance: wa.Instance = {}; - public client: WASocket; - private readonly localWebhook: wa.LocalWebHook = {}; - private readonly localChatwoot: wa.LocalChatwoot = {}; - private readonly localSettings: wa.LocalSettings = {}; - private readonly localWebsocket: wa.LocalWebsocket = {}; - private readonly localRabbitmq: wa.LocalRabbitmq = {}; - private readonly localSqs: wa.LocalSqs = {}; - public readonly localTypebot: wa.LocalTypebot = {}; - private readonly localProxy: wa.LocalProxy = {}; - private readonly localChamaai: wa.LocalChamaai = {}; - public stateConnection: wa.StateConnection = { state: 'close' }; public readonly storePath = join(ROOT_DIR, 'store'); - private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache(); - private endSession = false; - private logBaileys = this.configService.get('LOG').BAILEYS; + protected endSession = false; + protected phoneNumber: string; + protected readonly localWebhook: wa.LocalWebHook = {}; + protected readonly localChatwoot: wa.LocalChatwoot = {}; + protected readonly localSettings: wa.LocalSettings = {}; + protected readonly localWebsocket: wa.LocalWebsocket = {}; + protected readonly localRabbitmq: wa.LocalRabbitmq = {}; + protected readonly localSqs: wa.LocalSqs = {}; + public readonly localTypebot: wa.LocalTypebot = {}; + protected readonly localProxy: wa.LocalProxy = {}; + protected readonly localChamaai: wa.LocalChamaai = {}; + public stateConnection: any = { state: 'close' }; - private phoneNumber: string; + protected chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); - private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); + protected typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); + + protected chamaaiService = new ChamaaiService(waMonitor, this.configService); + + public async getProfileName(): Promise{} + public async getProfileStatus(): Promise{} + public async reloadConnection(): Promise {} + public async textMessage(data: SendTextDto, isChatwoot = false): Promise {} + public async mediaMessage(data: SendMediaDto, isChatwoot = false): Promise {} + public async mediaSticker(data: SendStickerDto): Promise {} + public async audioWhatsapp(data: SendAudioDto, isChatwoot = false): Promise {} + public async buttonMessage(data: SendButtonDto): Promise {} + public async locationMessage(data: SendLocationDto): Promise {} + public async listMessage(data: SendListDto): Promise {} + public async templateMessage(data: SendTemplateDto, isChatwoot = false): Promise {} + public async contactMessage(data: SendContactDto): Promise {} + public async reactionMessage(data: SendReactionDto): Promise {} + public async pollMessage(data: SendPollDto): Promise {} + public async statusMessage(data: SendStatusDto): Promise {} + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto): Promise {} + public async profilePicture(number: string): Promise {} + public async whatsappNumber(data: any): Promise {} + public async markMessageAsRead(data: any): Promise {} + public async archiveChat(data: any): Promise {} + public async deleteMessage(data: any): Promise {} + public async fetchProfile(instanceName: string, number?: string): Promise {} + public async fetchChats(): Promise {} + public async fetchContacts(query: ContactQuery): Promise {} + public async sendPresence(data: any): Promise {} + public async fetchStatusMessage(query: MessageUpQuery): Promise {} + public async fetchPrivacySettings(): Promise {} + public async updatePrivacySettings(data: any): Promise {} + public async fetchMessages(query: MessageQuery): Promise {} + public async fetchBusinessProfile(number: string): Promise {} + public async updateProfileName(name: string): Promise {} + public async updateProfileStatus(status: string): Promise {} + public async updateProfilePicture(picture: string): Promise {} + public async removeProfilePicture(): Promise {} + public async setWhatsappBusinessProfile(data: any): Promise {} + public async createGroup(create: CreateGroupDto): Promise {} + public async updateGroupPicture(picture: GroupPictureDto): Promise {} + public async updateGroupSubject(data: GroupSubjectDto): Promise {} + public async updateGroupDescription(data: GroupDescriptionDto): Promise {} + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out'): Promise {} + public async fetchAllGroups(getParticipants: GetParticipant): Promise {} + public async inviteCode(id: GroupJid): Promise {} + public async inviteInfo(id: GroupInvite): Promise {} + public async revokeInviteCode(id: GroupJid): Promise {} + public async findParticipants(id: GroupJid): Promise {} + public async updateGParticipant(update: GroupUpdateParticipantDto): Promise {} + public async updateGSetting(update: GroupUpdateSettingDto): Promise {} + public async toggleEphemeral(update: GroupToggleEphemeralDto): Promise {} + public async leaveGroup(id: GroupJid): Promise {} + public async sendInvite(id: GroupSendInvite): Promise {} + public async closeClient(): Promise {} - private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter); - private chamaaiService = new ChamaaiService(waMonitor, this.configService); public set instanceName(name: string) { + this.logger.verbose(`Initializing instance '${name}'`); if (!name) { this.logger.verbose('Instance name not found, generating random name with uuid'); @@ -203,57 +190,23 @@ export class WAStartupService { } } + public set instanceNumber(number: string) { + this.instance.number = number; + } + + public set instanceToken(token: string) { + this.instance.token = token; + } + public get instanceName() { this.logger.verbose('Getting instance name'); return this.instance.name; } - public get wuid() { this.logger.verbose('Getting remoteJid of instance'); return this.instance.wuid; } - public async getProfileName() { - this.logger.verbose('Getting profile name'); - - let profileName = this.client.user?.name ?? this.client.user?.verifiedName; - if (!profileName) { - this.logger.verbose('Profile name not found, trying to get from database'); - if (this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('Database enabled, trying to get from database'); - const collection = dbserver - .getClient() - .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(this.instanceName); - const data = await collection.findOne({ _id: 'creds' }); - if (data) { - this.logger.verbose('Profile name found in database'); - const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { - this.logger.verbose('Profile name found in file'); - const creds = JSON.parse( - readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { - encoding: 'utf-8', - }), - ); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } - - this.logger.verbose(`Profile name: ${profileName}`); - return profileName; - } - - public async getProfileStatus() { - this.logger.verbose('Getting profile status'); - const status = await this.client.fetchStatus(this.instance.wuid); - - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; - } - public get profilePictureUrl() { this.logger.verbose('Getting profile picture url'); return this.instance.profilePictureUrl; @@ -270,7 +223,7 @@ export class WAStartupService { }; } - private async loadWebhook() { + protected async loadWebhook() { this.logger.verbose('Loading webhook'); const data = await this.repository.webhook.find(this.instanceName); this.localWebhook.url = data?.url; @@ -321,7 +274,7 @@ export class WAStartupService { }; } - private async loadChatwoot() { + protected async loadChatwoot() { this.logger.verbose('Loading chatwoot'); const data = await this.repository.chatwoot.find(this.instanceName); this.localChatwoot.enabled = data?.enabled; @@ -402,7 +355,7 @@ export class WAStartupService { }; } - private async loadSettings() { + protected async loadSettings() { this.logger.verbose('Loading settings'); const data = await this.repository.settings.find(this.instanceName); this.localSettings.reject_call = data?.reject_call; @@ -437,8 +390,6 @@ export class WAStartupService { this.logger.verbose(`Settings read_status: ${data.read_status}`); Object.assign(this.localSettings, data); this.logger.verbose('Settings set'); - - this.client?.ws?.close(); } public async findSettings() { @@ -466,7 +417,7 @@ export class WAStartupService { }; } - private async loadWebsocket() { + protected async loadWebsocket() { this.logger.verbose('Loading websocket'); const data = await this.repository.websocket.find(this.instanceName); @@ -503,7 +454,7 @@ export class WAStartupService { }; } - private async loadRabbitmq() { + protected async loadRabbitmq() { this.logger.verbose('Loading rabbitmq'); const data = await this.repository.rabbitmq.find(this.instanceName); @@ -548,7 +499,7 @@ export class WAStartupService { } } - private async loadSqs() { + protected async loadSqs() { this.logger.verbose('Loading sqs'); const data = await this.repository.sqs.find(this.instanceName); @@ -593,7 +544,7 @@ export class WAStartupService { } } - private async loadTypebot() { + protected async loadTypebot() { this.logger.verbose('Loading typebot'); const data = await this.repository.typebot.find(this.instanceName); @@ -680,10 +631,6 @@ export class WAStartupService { this.logger.verbose(`Proxy proxy: ${data.proxy}`); Object.assign(this.localProxy, data); this.logger.verbose('Proxy set'); - - if (reload) { - this.reloadConnection(); - } } public async findProxy() { @@ -1086,220 +1033,7 @@ export class WAStartupService { } } - private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { - this.logger.verbose('Connection update'); - if (qr) { - this.logger.verbose('QR code found'); - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { - this.logger.verbose('QR code limit reached'); - - this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); - this.sendDataWebhook(Events.QRCODE_UPDATED, { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }, - ); - } - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - state: 'refused', - statusReason: DisconnectReason.connectionClosed, - }); - - this.logger.verbose('endSession defined as true'); - this.endSession = true; - - this.logger.verbose('Emmiting event logout.instance'); - return this.eventEmitter.emit('no.connection', this.instance.name); - } - - this.logger.verbose('Incrementing QR code count'); - this.instance.qrcode.count++; - - const color = this.configService.get('QRCODE').COLOR; - - const optsQrcode: QRCodeToDataURLOptions = { - margin: 3, - scale: 4, - errorCorrectionLevel: 'H', - color: { light: '#ffffff', dark: color }, - }; - - if (this.phoneNumber) { - await delay(2000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); - } else { - this.instance.qrcode.pairingCode = null; - } - - this.logger.verbose('Generating QR code'); - qrcode.toDataURL(qr, optsQrcode, (error, base64) => { - if (error) { - this.logger.error('Qrcode generate failed:' + error.toString()); - return; - } - - this.instance.qrcode.base64 = base64; - this.instance.qrcode.code = qr; - - this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }, - ); - } - }); - - this.logger.verbose('Generating QR code in terminal'); - qrcodeTerminal.generate(qr, { small: true }, (qrcode) => - this.logger.log( - `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), - ); - } - - if (connection) { - this.logger.verbose('Connection found'); - this.stateConnection = { - state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, - }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - ...this.stateConnection, - }); - } - - if (connection === 'close') { - this.logger.verbose('Connection closed'); - const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - this.logger.verbose('Reconnecting to whatsapp'); - await this.connectToWhatsapp(); - } else { - this.logger.verbose('Do not reconnect to whatsapp'); - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'closed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'closed', - }, - ); - } - - this.logger.verbose('Emittin event logout.instance'); - this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); - this.client?.ws?.close(); - this.client.end(new Error('Close connection')); - this.logger.verbose('Connection closed'); - } - } - - if (connection === 'open') { - this.logger.verbose('Connection opened'); - this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); - this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; - const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' '); - const formattedName = this.instance.name; - this.logger.info( - ` - ┌──────────────────────────────┐ - │ CONNECTED TO WHATSAPP │ - └──────────────────────────────┘`.replace(/^ +/gm, ' '), - ); - this.logger.info(` - wuid: ${formattedWuid} - name: ${formattedName} - `); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.CONNECTION_UPDATE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'open', - }, - ); - } - } - } - - private async getMessage(key: proto.IMessageKey, full = false) { - this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); - try { - const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.name, key: { id: key.id } }, - })) as unknown as proto.IWebMessageInfo[]; - if (full) { - this.logger.verbose('Returning full message'); - return webMessageInfo[0]; - } - if (webMessageInfo[0].message?.pollCreationMessage) { - this.logger.verbose('Returning poll message'); - const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; - - if (typeof messageSecretBase64 === 'string') { - const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - - const msg = { - messageContextInfo: { - messageSecret, - }, - pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, - }; - - return msg; - } - } - - this.logger.verbose('Returning message'); - return webMessageInfo[0].message; - } catch (error) { - return { conversation: '' }; - } - } - - private cleanStore() { + protected cleanStore() { this.logger.verbose('Cronjob to clean store initialized'); const cleanStore = this.configService.get('CLEAN_STORE'); const database = this.configService.get('DATABASE'); @@ -1324,7 +1058,7 @@ export class WAStartupService { } } - private cleanDB() { + protected cleanDB() { this.logger.verbose('Cronjob to clean db initialized'); const database = this.configService.get('DATABASE'); if (database?.CLEANING_DB_INTERVAL && database.ENABLED) { @@ -1341,9 +1075,14 @@ export class WAStartupService { } } - private async defineAuthState() { + // Instance Controller + public get connectionStatus() { + this.logger.verbose('Getting connection status'); + return this.stateConnection; + } + + protected async defineAuthState() { this.logger.verbose('Defining auth state'); - const db = this.configService.get('DATABASE'); const redis = this.configService.get('REDIS'); if (redis?.ENABLED) { @@ -1351,2421 +1090,24 @@ export class WAStartupService { this.cache.reference = this.instance.name; return await useMultiFileAuthStateRedisDb(this.cache); } - - if (db.SAVE_DATA.INSTANCE && db.ENABLED) { - this.logger.verbose('Database enabled'); - return await useMultiFileAuthStateDb(this.instance.name); - } - - this.logger.verbose('Store file enabled'); - return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); } - public async connectToWhatsapp(number?: string): Promise { + public async connectToWhatsapp(data?: any): Promise< any > { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); this.loadChatwoot(); - this.loadSettings(); this.loadWebsocket(); this.loadRabbitmq(); this.loadSqs(); this.loadTypebot(); - this.loadProxy(); this.loadChamaai(); - - this.instance.authState = await this.defineAuthState(); - - const { version } = await fetchLatestBaileysVersion(); - this.logger.verbose('Baileys version: ' + version); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); - - let options; - - if (this.localProxy.enabled) { - this.logger.info('Proxy enabled: ' + this.localProxy.proxy); - - if (this.localProxy.proxy.includes('proxyscrape')) { - const response = await axios.get(this.localProxy.proxy); - const text = response.data; - const proxyUrls = text.split('\r\n'); - const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); - const proxyUrl = 'http://' + proxyUrls[rand]; - options = { - agent: new ProxyAgent(proxyUrl as any), - }; - } else { - options = { - agent: new ProxyAgent(this.localProxy.proxy as any), - }; - } - } - - const socketConfig: UserFacingSocketConfig = { - ...options, - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - retryRequestDelayMs: 10, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - shouldIgnoreJid: (jid) => { - const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); - const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); - - return isGroupJid || isBroadcast; - }, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: false, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; - } - - return message; - }, - }; - - this.endSession = false; - - this.logger.verbose('Creating socket'); - - this.client = makeWASocket(socketConfig); - - this.logger.verbose('Socket created'); - - this.eventHandler(); - - this.logger.verbose('Socket event handler initialized'); - - this.phoneNumber = number; - - return this.client; + + return } catch (error) { this.logger.error(error); throw new InternalServerErrorException(error?.toString()); } } - public async reloadConnection(): Promise { - try { - this.instance.authState = await this.defineAuthState(); - - const { version } = await fetchLatestBaileysVersion(); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - - let options; - - if (this.localProxy.enabled) { - this.logger.verbose('Proxy enabled'); - options = { - agent: new ProxyAgent(this.localProxy.proxy as any), - fetchAgent: new ProxyAgent(this.localProxy.proxy as any), - }; - } - - const socketConfig: UserFacingSocketConfig = { - ...options, - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - retryRequestDelayMs: 10, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - shouldIgnoreJid: (jid) => { - const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); - const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); - - return isGroupJid || isBroadcast; - }, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: false, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; - } - - return message; - }, - }; - - this.client = makeWASocket(socketConfig); - - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } - - private readonly chatHandle = { - 'chats.upsert': async (chats: Chat[], database: Database) => { - this.logger.verbose('Event received: chats.upsert'); - - this.logger.verbose('Finding chats in database'); - const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if chats exists in database to insert'); - const chatsRaw: ChatRaw[] = []; - for await (const chat of chats) { - if (chatsRepository.find((cr) => cr.id === chat.id)) { - continue; - } - - chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); - } - - this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - }, - - 'chats.update': async ( - chats: Partial< - proto.IConversation & { - lastMessageRecvTimestamp?: number; - } & { - conditional: (bufferedData: BufferedEventData) => boolean; - } - >[], - ) => { - this.logger.verbose('Event received: chats.update'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { id: chat.id, owner: this.instance.wuid }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); - }, - - 'chats.delete': async (chats: string[]) => { - this.logger.verbose('Event received: chats.delete'); - - this.logger.verbose('Deleting chats in database'); - chats.forEach( - async (chat) => - await this.repository.chat.delete({ - where: { owner: this.instance.name, id: chat }, - }), - ); - - this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); - }, - }; - - private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[], database: Database) => { - this.logger.verbose('Event received: contacts.upsert'); - - this.logger.verbose('Finding contacts in database'); - const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - if (contactsRepository.find((cr) => cr.id === contact.id)) { - continue; - } - - contactsRaw.push({ - id: contact.id, - pushName: contact?.name || contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); - - this.logger.verbose('Inserting contacts in database'); - this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - - 'contacts.update': async (contacts: Partial[], database: Database) => { - this.logger.verbose('Event received: contacts.update'); - - this.logger.verbose('Verifying if contacts exists in database to update'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - contactsRaw.push({ - id: contact.id, - pushName: contact?.name ?? contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - - this.logger.verbose('Updating contacts in database'); - this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - }; - - private readonly messageHandle = { - 'messaging-history.set': async ( - { - messages, - chats, - isLatest, - }: { - chats: Chat[]; - contacts: Contact[]; - messages: proto.IWebMessageInfo[]; - isLatest: boolean; - }, - database: Database, - ) => { - this.logger.verbose('Event received: messaging-history.set'); - if (isLatest) { - this.logger.verbose('isLatest defined as true'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { - id: chat.id, - owner: this.instance.name, - lastMsgTimestamp: chat.lastMessageRecvTimestamp, - }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_SET'); - this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - } - - const messagesRaw: MessageRaw[] = []; - const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.name }, - }); - for await (const [, m] of Object.entries(messages)) { - if (!m.message) { - continue; - } - if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { - continue; - } - - if (Long.isLong(m?.messageTimestamp)) { - m.messageTimestamp = m.messageTimestamp?.toNumber(); - } - - messagesRaw.push({ - key: m.key, - pushName: m.pushName, - participant: m.participant, - message: { ...m.message }, - messageType: getContentType(m.message), - messageTimestamp: m.messageTimestamp as number, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); - - messages = undefined; - }, - - 'messages.upsert': async ( - { - messages, - type, - }: { - messages: proto.IWebMessageInfo[]; - type: MessageUpsertType; - }, - database: Database, - settings: SettingsRaw, - ) => { - try { - this.logger.verbose('Event received: messages.upsert'); - for (const received of messages) { - if ( - (type !== 'notify' && type !== 'append') || - received.message?.protocolMessage || - received.message?.pollUpdateMessage - ) { - this.logger.verbose('message rejected'); - return; - } - - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } - - if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - - let messageRaw: MessageRaw; - - if ( - (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || - received?.message?.imageMessage - ) { - const buffer = await downloadMediaMessage( - { key: received.key, message: received?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }) as any, - reuploadRequest: this.client.updateMediaMessage, - }, - ); - messageRaw = { - key: received.key, - pushName: received.pushName, - message: { - ...received.message, - base64: buffer ? buffer.toString('base64') : undefined, - }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - } else { - messageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - } - - if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - if (this.localSettings.read_status && received.key.id === 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - if (this.localChatwoot.enabled && !received.key.id.includes('@broadcast')) { - const chatwootSentMessage = await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - - if (chatwootSentMessage?.id) { - messageRaw.chatwoot = { - messageId: chatwootSentMessage.id, - inboxId: chatwootSentMessage.inbox_id, - conversationId: chatwootSentMessage.conversation_id, - }; - } - } - - const typebotSessionRemoteJid = this.localTypebot.sessions?.find( - (session) => session.remoteJid === received.key.remoteJid, - ); - - if ((this.localTypebot.enabled && type === 'notify') || typebotSessionRemoteJid) { - if (!(this.localTypebot.listening_from_me === false && messageRaw.key.fromMe === true)) { - if (messageRaw.messageType !== 'reactionMessage') - await this.typebotService.sendTypebot( - { instanceName: this.instance.name }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - } - - if (this.localChamaai.enabled && messageRaw.key.fromMe === false && type === 'notify') { - await this.chamaaiService.sendChamaai( - { instanceName: this.instance.name }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - } - } catch (error) { - this.logger.error(error); - } - }, - - 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { - this.logger.verbose('Event received: messages.update'); - const status: Record = { - 0: 'ERROR', - 1: 'PENDING', - 2: 'SERVER_ACK', - 3: 'DELIVERY_ACK', - 4: 'READ', - 5: 'PLAYED', - }; - for await (const { key, update } of args) { - if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { - this.logger.verbose('Message update is valid'); - - let pollUpdates: any; - if (update.pollUpdates) { - this.logger.verbose('Poll update found'); - - this.logger.verbose('Getting poll message'); - const pollCreation = await this.getMessage(key); - this.logger.verbose(pollCreation); - - if (pollCreation) { - this.logger.verbose('Getting aggregate votes in poll message'); - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); - } - } - - if (status[update.status] === 'READ' && !key.fromMe) return; - - if (update.message === null && update.status === undefined) { - this.logger.verbose('Message deleted'); - - this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - this.sendDataWebhook(Events.MESSAGES_DELETE, key); - - const message: MessageUpdateRaw = { - ...key, - status: 'DELETED', - datetime: Date.now(), - owner: this.instance.name, - }; - - this.logger.verbose(message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.MESSAGES_DELETE, - { instanceName: this.instance.name }, - { key: key }, - ); - } - - return; - } - - const message: MessageUpdateRaw = { - ...key, - status: status[update.status], - datetime: Date.now(), - owner: this.instance.name, - pollUpdates, - }; - - this.logger.verbose(message); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - - this.logger.verbose('Inserting message in database'); - this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); - } - } - }, - }; - - private readonly groupHandler = { - 'groups.upsert': (groupMetadata: GroupMetadata[]) => { - this.logger.verbose('Event received: groups.upsert'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); - this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); - }, - - 'groups.update': (groupMetadataUpdate: Partial[]) => { - this.logger.verbose('Event received: groups.update'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); - this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); - }, - - 'group-participants.update': (participantsUpdate: { - id: string; - participants: string[]; - action: ParticipantAction; - }) => { - this.logger.verbose('Event received: group-participants.update'); - - this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - }, - }; - - private eventHandler() { - this.logger.verbose('Initializing event handler'); - this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - this.logger.verbose('Listening event: call'); - const call = events.call[0]; - - if (settings?.reject_call && call.status == 'offer') { - this.logger.verbose('Rejecting call'); - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msg_call?.trim().length > 0 && call.status == 'offer') { - this.logger.verbose('Sending message in call'); - const msg = await this.client.sendMessage(call.from, { - text: settings.msg_call, - }); - - this.logger.verbose('Sending data to event messages.upsert'); - this.client.ev.emit('messages.upsert', { - messages: [msg], - type: 'notify', - }); - } - - this.logger.verbose('Sending data to webhook in event CALL'); - this.sendDataWebhook(Events.CALL, call); - } - - if (events['connection.update']) { - this.logger.verbose('Listening event: connection.update'); - this.connectionUpdate(events['connection.update']); - } - - if (events['creds.update']) { - this.logger.verbose('Listening event: creds.update'); - this.instance.authState.saveCreds(); - } - - if (events['messaging-history.set']) { - this.logger.verbose('Listening event: messaging-history.set'); - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload, database); - } - - if (events['messages.upsert']) { - this.logger.verbose('Listening event: messages.upsert'); - const payload = events['messages.upsert']; - if (payload.messages.find((a) => a?.messageStubType === 2)) { - const msg = payload.messages[0]; - retryCache[msg.key.id] = msg; - return; - } - this.messageHandle['messages.upsert'](payload, database, settings); - } - - if (events['messages.update']) { - this.logger.verbose('Listening event: messages.update'); - const payload = events['messages.update']; - payload.forEach((message) => { - if (retryCache[message.key.id]) { - this.client.ev.emit('messages.upsert', { - messages: [message], - type: 'notify', - }); - delete retryCache[message.key.id]; - return; - } - }); - this.messageHandle['messages.update'](payload, database, settings); - } - - if (events['presence.update']) { - this.logger.verbose('Listening event: presence.update'); - const payload = events['presence.update']; - - if (settings.groups_ignore && payload.id.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } - - if (!settings?.groups_ignore) { - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } - - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } - - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); - } - } - - if (events['chats.upsert']) { - this.logger.verbose('Listening event: chats.upsert'); - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload, database); - } - - if (events['chats.update']) { - this.logger.verbose('Listening event: chats.update'); - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } - - if (events['chats.delete']) { - this.logger.verbose('Listening event: chats.delete'); - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } - - if (events['contacts.upsert']) { - this.logger.verbose('Listening event: contacts.upsert'); - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload, database); - } - - if (events['contacts.update']) { - this.logger.verbose('Listening event: contacts.update'); - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload, database); - } - } - }); - } - - // Check if the number is MX or AR - private formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; - } - return jid; - } - - // Check if the number is br - private formatBRNumber(jid: string) { - const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); - if (regexp.test(jid)) { - const match = regexp.exec(jid); - if (match && match[1] === '55') { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 31) { - return match[0]; - } - return match[1] + match[2] + match[3]; - } - return jid; - } else { - return jid; - } - } - - private createJid(number: string): string { - this.logger.verbose('Creating jid with number: ' + number); - - if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net or @lid'); - return number; - } - - if (number.includes('@broadcast')) { - this.logger.verbose('Number already contains @broadcast'); - return number; - } - - number = number - ?.replace(/\s/g, '') - .replace(/\+/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .split(':')[0] - .split('@')[0]; - - if (number.includes('-') && number.length >= 24) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - number = number.replace(/\D/g, ''); - - if (number.length >= 18) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); - return `${number}@s.whatsapp.net`; - } - - public async profilePicture(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile picture with jid: ' + jid); - try { - this.logger.verbose('Getting profile picture url'); - return { - wuid: jid, - profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), - }; - } catch (error) { - this.logger.verbose('Profile picture not found'); - return { - wuid: jid, - profilePictureUrl: null, - }; - } - } - - public async getStatus(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } - - public async fetchProfile(instanceName: string, number?: string) { - const jid = number ? this.createJid(number) : this.client?.user?.id; - - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(info?.jid); - const status = await this.getStatus(info?.jid); - const business = await this.fetchBusinessProfile(info?.jid); - - return { - wuid: info?.jid || jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - const business = await this.fetchBusinessProfile(jid); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } - - private async sendMessageWithTyping( - number: string, - message: T, - options?: Options, - isChatwoot = false, - ) { - this.logger.verbose('Sending message with typing'); - - this.logger.verbose(`Check if number "${number}" is WhatsApp`); - const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); - - this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - try { - if (options?.delay) { - this.logger.verbose('Delaying message'); - - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); - this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); - - await delay(options.delay); - this.logger.verbose('Set delay: ' + options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } - - const linkPreview = options?.linkPreview != false ? undefined : false; - - let quoted: WAMessage; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - this.logger.verbose('Quoted message'); - } - - let mentions: string[]; - if (isJidGroup(sender)) { - try { - const group = await this.findGroup({ groupJid: sender }, 'inner'); - - if (!group) { - throw new NotFoundException('Group not found'); - } - - if (options?.mentions) { - this.logger.verbose('Mentions defined'); - - if (options.mentions?.everyOne) { - this.logger.verbose('Mentions everyone'); - - this.logger.verbose('Getting group metadata'); - mentions = group.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else if (options.mentions?.mentioned?.length) { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - return null; - } - return jid; - }); - } - } - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - - const messageSent = await (async () => { - const option = { - quoted, - }; - - if ( - !message['audio'] && - !message['poll'] && - !message['sticker'] && - !message['conversation'] && - sender !== 'status@broadcast' - ) { - if (message['reactionMessage']) { - this.logger.verbose('Sending reaction'); - return await this.client.sendMessage( - sender, - { - react: { - text: message['reactionMessage']['text'], - key: message['reactionMessage']['key'], - }, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } - } - if (message['conversation']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - text: message['conversation'], - mentions, - linkPreview: linkPreview, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (sender === 'status@broadcast') { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message['status'].content as unknown as AnyMessageContent, - { - backgroundColor: message['status'].option.backgroundColor, - font: message['status'].option.font, - statusJidList: message['status'].option.statusJidList, - } as unknown as MiscMessageGenerationOptions, - ); - } - - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - })(); - - const messageRaw: MessageRaw = { - key: messageSent.key, - pushName: messageSent.pushName, - message: { ...messageSent.message }, - messageType: getContentType(messageSent.message), - messageTimestamp: messageSent.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(messageSent.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - if (this.localChatwoot.enabled && !isChatwoot) { - this.chatwootService.eventWhatsapp(Events.SEND_MESSAGE, { instanceName: this.instance.name }, messageRaw); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, - ); - - return messageSent; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Instance Controller - public get connectionStatus() { - this.logger.verbose('Getting connection status'); - return this.stateConnection; - } - - public async sendPresence(data: SendPresenceDto) { - try { - const { number } = data; - - this.logger.verbose(`Check if number "${number}" is WhatsApp`); - const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); - - this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - this.logger.verbose('Sending presence'); - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); - this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing'); - - await delay(data.options.delay); - this.logger.verbose('Set delay: ' + data.options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Send Message Controller - public async textMessage(data: SendTextDto, isChatwoot = false) { - this.logger.verbose('Sending text message'); - return await this.sendMessageWithTyping( - data.number, - { - conversation: data.textMessage.text, - }, - data?.options, - isChatwoot, - ); - } - - public async pollMessage(data: SendPollDto) { - this.logger.verbose('Sending poll message'); - return await this.sendMessageWithTyping( - data.number, - { - poll: { - name: data.pollMessage.name, - selectableCount: data.pollMessage.selectableCount, - values: data.pollMessage.values, - }, - }, - data?.options, - ); - } - - private async formatStatusMessage(status: StatusMessage) { - this.logger.verbose('Formatting status message'); - - if (!status.type) { - throw new BadRequestException('Type is required'); - } - - if (!status.content) { - throw new BadRequestException('Content is required'); - } - - if (status.allContacts) { - this.logger.verbose('All contacts defined as true'); - - this.logger.verbose('Getting contacts from database'); - const contacts = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - if (!contacts.length) { - throw new BadRequestException('Contacts not found'); - } - - this.logger.verbose('Getting contacts with push name'); - status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); - - this.logger.verbose(status.statusJidList); - } - - if (!status.statusJidList?.length && !status.allContacts) { - throw new BadRequestException('StatusJidList is required'); - } - - if (status.type === 'text') { - this.logger.verbose('Type defined as text'); - - if (!status.backgroundColor) { - throw new BadRequestException('Background color is required'); - } - - if (!status.font) { - throw new BadRequestException('Font is required'); - } - - return { - content: { - text: status.content, - }, - option: { - backgroundColor: status.backgroundColor, - font: status.font, - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'image') { - this.logger.verbose('Type defined as image'); - - return { - content: { - image: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'video') { - this.logger.verbose('Type defined as video'); - - return { - content: { - video: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'audio') { - this.logger.verbose('Type defined as audio'); - - this.logger.verbose('Processing audio'); - const convert = await this.processAudio(status.content, 'status@broadcast'); - if (typeof convert === 'string') { - this.logger.verbose('Audio processed'); - const audio = fs.readFileSync(convert).toString('base64'); - - const result = { - content: { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - option: { - statusJidList: status.statusJidList, - }, - }; - - fs.unlinkSync(convert); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - throw new BadRequestException('Type not found'); - } - - public async statusMessage(data: SendStatusDto) { - this.logger.verbose('Sending status message'); - const status = await this.formatStatusMessage(data.statusMessage); - - return await this.sendMessageWithTyping('status@broadcast', { - status, - }); - } - - private async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - this.logger.verbose('Preparing media message'); - const prepareMedia = await prepareWAMessageMedia( - { - [mediaMessage.mediatype]: isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'), - } as any, - { upload: this.client.waUploadToServer }, - ); - - const mediaType = mediaMessage.mediatype + 'Message'; - this.logger.verbose('Media type: ' + mediaType); - - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - this.logger.verbose('If media type is document and file name is not defined then'); - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - this.logger.verbose('File name: ' + mediaMessage.fileName); - } - - if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) { - mediaMessage.fileName = 'image.png'; - } - - if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) { - mediaMessage.fileName = 'video.mp4'; - } - - let mimetype: string; - - if (mediaMessage.mimetype) { - mimetype = mediaMessage.mimetype; - } else { - if (isURL(mediaMessage.media)) { - const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' }); - - mimetype = response.headers['content-type']; - } else { - mimetype = getMIMEType(mediaMessage.fileName); - } - } - - this.logger.verbose('Mimetype: ' + mimetype); - - prepareMedia[mediaType].caption = mediaMessage?.caption; - prepareMedia[mediaType].mimetype = mimetype; - prepareMedia[mediaType].fileName = mediaMessage.fileName; - - if (mediaMessage.mediatype === 'video') { - this.logger.verbose('Is media type video then set gif playback as false'); - prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( - readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), - ); - prepareMedia[mediaType].gifPlayback = false; - } - - this.logger.verbose('Generating wa message from content'); - return generateWAMessageFromContent( - '', - { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, - ); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } - } - - private async convertToWebP(image: string, number: string) { - try { - this.logger.verbose('Converting image to WebP to sticker'); - - let imagePath: string; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to image name: ' + hash); - - const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; - this.logger.verbose('Output path: ' + outputPath); - - if (isBase64(image)) { - this.logger.verbose('Image is base64'); - - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); - const imageBuffer = Buffer.from(base64Data, 'base64'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } else { - this.logger.verbose('Image is url'); - - const timestamp = new Date().getTime(); - const url = `${image}?timestamp=${timestamp}`; - this.logger.verbose('including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting image from url'); - - const imageBuffer = Buffer.from(response.data, 'binary'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } - - await sharp(imagePath).webp().toFile(outputPath); - this.logger.verbose('Image converted to WebP'); - - fs.unlinkSync(imagePath); - this.logger.verbose('Temp image deleted'); - - return outputPath; - } catch (error) { - console.error('Erro ao converter a imagem para WebP:', error); - } - } - - public async mediaSticker(data: SendStickerDto) { - this.logger.verbose('Sending media sticker'); - const convert = await this.convertToWebP(data.stickerMessage.image, data.number); - const result = await this.sendMessageWithTyping( - data.number, - { - sticker: { url: convert }, - }, - data?.options, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted image deleted'); - - return result; - } - - public async mediaMessage(data: SendMediaDto, isChatwoot = false) { - this.logger.verbose('Sending media message'); - const generate = await this.prepareMediaMessage(data.mediaMessage); - - return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options, isChatwoot); - } - - public async processAudio(audio: string, number: string) { - this.logger.verbose('Processing audio'); - let tempAudioPath: string; - let outputAudio: string; - - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to audio name: ' + hash); - - if (isURL(audio)) { - this.logger.verbose('Audio is url'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const timestamp = new Date().getTime(); - const url = `${audio}?timestamp=${timestamp}`; - - this.logger.verbose('Including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting audio from url'); - - fs.writeFileSync(tempAudioPath, response.data); - } else { - this.logger.verbose('Audio is base64'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const audioBuffer = Buffer.from(audio, 'base64'); - fs.writeFileSync(tempAudioPath, audioBuffer); - this.logger.verbose('Temp audio created'); - } - - this.logger.verbose('Converting audio to mp4'); - return new Promise((resolve, reject) => { - exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { - fs.unlinkSync(tempAudioPath); - this.logger.verbose('Temp audio deleted'); - - if (error) reject(error); - - this.logger.verbose('Audio converted to mp4'); - resolve(outputAudio); - }); - }); - } - - public async audioWhatsapp(data: SendAudioDto, isChatwoot = false) { - this.logger.verbose('Sending audio whatsapp'); - - if (!data.options?.encoding && data.options?.encoding !== false) { - data.options.encoding = true; - } - - if (data.options?.encoding) { - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - isChatwoot, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - return await this.sendMessageWithTyping( - data.number, - { - audio: isURL(data.audioMessage.audio) - ? { url: data.audioMessage.audio } - : Buffer.from(data.audioMessage.audio, 'base64'), - ptt: true, - mimetype: 'audio/ogg; codecs=opus', - }, - { presence: 'recording', delay: data?.options?.delay }, - isChatwoot, - ); - } - - public async buttonMessage(data: SendButtonDto) { - this.logger.verbose('Sending button message'); - const embeddedMedia: any = {}; - let mediatype = 'TEXT'; - - if (data.buttonMessage?.mediaMessage) { - mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; - embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; - const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); - embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; - embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; - } - - const btnItems = { - text: data.buttonMessage.buttons.map((btn) => btn.buttonText), - ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), - }; - - 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, - { - buttonsMessage: { - text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, - contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, - footerText: data.buttonMessage?.footerText, - buttons: data.buttonMessage.buttons.map((button) => { - return { - buttonText: { - displayText: button.buttonText, - }, - buttonId: button.buttonId, - type: 1, - }; - }), - headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], - [embeddedMedia?.mediaKey]: embeddedMedia?.message, - }, - }, - data?.options, - ); - } - - public async locationMessage(data: SendLocationDto) { - this.logger.verbose('Sending location message'); - return await this.sendMessageWithTyping( - data.number, - { - locationMessage: { - degreesLatitude: data.locationMessage.latitude, - degreesLongitude: data.locationMessage.longitude, - name: data.locationMessage?.name, - address: data.locationMessage?.address, - }, - }, - data?.options, - ); - } - - public async listMessage(data: SendListDto) { - this.logger.verbose('Sending list message'); - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.listMessage.title, - description: data.listMessage.description, - buttonText: data.listMessage?.buttonText, - footerText: data.listMessage?.footerText, - sections: data.listMessage.sections, - listType: 1, - }, - }, - data?.options, - ); - } - - public async contactMessage(data: SendContactDto) { - this.logger.verbose('Sending contact message'); - const message: proto.IMessage = {}; - - const vcard = (contact: ContactMessage) => { - this.logger.verbose('Creating vcard'); - let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; - - if (contact.organization) { - this.logger.verbose('Organization defined'); - result += `ORG:${contact.organization};\n`; - } - - if (contact.email) { - this.logger.verbose('Email defined'); - result += `EMAIL:${contact.email}\n`; - } - - if (contact.url) { - this.logger.verbose('Url defined'); - result += `URL:${contact.url}\n`; - } - - if (!contact.wuid) { - this.logger.verbose('Wuid defined'); - contact.wuid = this.createJid(contact.phoneNumber); - } - - result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; - - this.logger.verbose('Vcard created'); - return result; - }; - - if (data.contactMessage.length === 1) { - message.contactMessage = { - displayName: data.contactMessage[0].fullName, - vcard: vcard(data.contactMessage[0]), - }; - } else { - message.contactsArrayMessage = { - displayName: `${data.contactMessage.length} contacts`, - contacts: data.contactMessage.map((contact) => { - return { - displayName: contact.fullName, - vcard: vcard(contact), - }; - }), - }; - } - - return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); - } - - public async reactionMessage(data: SendReactionDto) { - this.logger.verbose('Sending reaction message'); - return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { - reactionMessage: { - key: data.reactionMessage.key, - text: data.reactionMessage.reaction, - }, - }); - } - - // Chat Controller - public async whatsappNumber(data: WhatsAppNumberDto) { - this.logger.verbose('Getting whatsapp number'); - - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); - - if (isJidGroup(jid)) { - const group = await this.findGroup({ groupJid: jid }, 'inner'); - - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); - } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); - } - } - } - - return onWhatsapp; - } - - public async markMessageAsRead(data: ReadMessageDto) { - this.logger.verbose('Marking message as read'); - - try { - const keys: proto.IMessageKey[] = []; - data.read_messages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { - keys.push({ - remoteJid: read.remoteJid, - fromMe: read.fromMe, - id: read.id, - }); - } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); - } - } - - public async getLastMessage(number: string) { - const messages = await this.fetchMessages({ - where: { - key: { - remoteJid: number, - }, - owner: this.instance.name, - }, - }); - - let lastMessage = messages.pop(); - - for (const message of messages) { - if (message.messageTimestamp >= lastMessage.messageTimestamp) { - lastMessage = message; - } - } - - return lastMessage as unknown as LastMessage; - } - - public async archiveChat(data: ArchiveChatDto) { - this.logger.verbose('Archiving chat'); - try { - let last_message = data.lastMessage; - let number = data.chat; - - if (!last_message && number) { - last_message = await this.getLastMessage(number); - } else { - last_message = data.lastMessage; - last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); - number = last_message?.key?.remoteJid; - } - - if (!last_message || Object.keys(last_message).length === 0) { - throw new NotFoundException('Last message not found'); - } - - await this.client.chatModify( - { - archive: data.archive, - lastMessages: [last_message], - }, - this.createJid(number), - ); - - return { - chatId: number, - archived: true, - }; - } catch (error) { - throw new InternalServerErrorException({ - archived: false, - message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], - }); - } - } - - public async deleteMessage(del: DeleteMessage) { - this.logger.verbose('Deleting message'); - try { - return await this.client.sendMessage(del.remoteJid, { delete: del }); - } catch (error) { - throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); - } - } - - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { - this.logger.verbose('Getting base64 from media message'); - try { - const m = data?.message; - const convertToMp4 = data?.convertToMp4 ?? false; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - for (const subtype of MessageSubtype) { - if (msg.message[subtype]) { - msg.message = msg.message[subtype].message; - } - } - - let mediaMessage: any; - let mediaType: string; - - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; - } - } - - if (!mediaMessage) { - throw 'The message is not of the media type'; - } - - if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message = JSON.parse(JSON.stringify(msg.message)); - } - - this.logger.verbose('Downloading media message'); - const buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }) as any, - reuploadRequest: this.client.updateMediaMessage, - }, - ); - const typeMessage = getContentType(msg.message); - - if (convertToMp4 && typeMessage === 'audioMessage') { - this.logger.verbose('Converting audio to mp4'); - const number = msg.key.remoteJid.split('@')[0]; - const convert = await this.processAudio(buffer.toString('base64'), number); - - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - this.logger.verbose('Audio converted to mp4'); - - const result = { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: 'audio/mp4', - base64: Buffer.from(audio, 'base64').toString('base64'), - }; - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - this.logger.verbose('Media message downloaded'); - return result; - } - } - - this.logger.verbose('Media message downloaded'); - return { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: mediaMessage['mimetype'], - base64: buffer.toString('base64'), - }; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - public async fetchContacts(query: ContactQuery) { - this.logger.verbose('Fetching contacts'); - if (query?.where) { - query.where.owner = this.instance.name; - if (query.where?.id) { - query.where.id = this.createJid(query.where.id); - } - } else { - query = { - where: { - owner: this.instance.name, - }, - }; - } - return await this.repository.contact.find(query); - } - - public async fetchMessages(query: MessageQuery) { - this.logger.verbose('Fetching messages'); - if (query?.where) { - if (query.where?.key?.remoteJid) { - query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.message.find(query); - } - - public async fetchStatusMessage(query: MessageUpQuery) { - this.logger.verbose('Fetching status messages'); - if (query?.where) { - if (query.where?.remoteJid) { - query.where.remoteJid = this.createJid(query.where.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.messageUpdate.find(query); - } - - public async fetchChats() { - this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.name } }); - } - - public async fetchPrivacySettings() { - this.logger.verbose('Fetching privacy settings'); - const privacy = await this.client.fetchPrivacySettings(); - - return { - readreceipts: privacy.readreceipts, - profile: privacy.profile, - status: privacy.status, - online: privacy.online, - last: privacy.last, - groupadd: privacy.groupadd, - }; - } - - public async updatePrivacySettings(settings: PrivacySettingDto) { - this.logger.verbose('Updating privacy settings'); - try { - await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); - this.logger.verbose('Read receipts privacy updated'); - - await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); - this.logger.verbose('Profile picture privacy updated'); - - await this.client.updateStatusPrivacy(settings.privacySettings.status); - this.logger.verbose('Status privacy updated'); - - await this.client.updateOnlinePrivacy(settings.privacySettings.online); - this.logger.verbose('Online privacy updated'); - - await this.client.updateLastSeenPrivacy(settings.privacySettings.last); - this.logger.verbose('Last seen privacy updated'); - - await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); - this.logger.verbose('Groups add privacy updated'); - - this.reloadConnection(); - - return { - update: 'success', - data: { - readreceipts: settings.privacySettings.readreceipts, - profile: settings.privacySettings.profile, - status: settings.privacySettings.status, - online: settings.privacySettings.online, - last: settings.privacySettings.last, - groupadd: settings.privacySettings.groupadd, - }, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating privacy settings', error.toString()); - } - } - - public async fetchBusinessProfile(number: string): Promise { - this.logger.verbose('Fetching business profile'); - try { - const jid = number ? this.createJid(number) : this.instance.wuid; - - const profile = await this.client.getBusinessProfile(jid); - this.logger.verbose('Trying to get business profile'); - - if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); - - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift(), - }; - } - - this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileName(name: string) { - this.logger.verbose('Updating profile name to ' + name); - try { - await this.client.updateProfileName(name); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileStatus(status: string) { - this.logger.verbose('Updating profile status to: ' + status); - try { - await this.client.updateProfileStatus(status); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile status', error.toString()); - } - } - - public async updateProfilePicture(picture: string) { - this.logger.verbose('Updating profile picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - - await this.client.updateProfilePicture(this.instance.wuid, pic); - this.logger.verbose('Profile picture updated'); - - this.reloadConnection(); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile picture', error.toString()); - } - } - - public async removeProfilePicture() { - this.logger.verbose('Removing profile picture'); - try { - await this.client.removeProfilePicture(this.instance.wuid); - - this.reloadConnection(); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error removing profile picture', error.toString()); - } - } - - // Group - public async createGroup(create: CreateGroupDto) { - this.logger.verbose('Creating group: ' + create.subject); - try { - const participants = (await this.whatsappNumber({ numbers: create.participants })) - .filter((participant) => participant.exists) - .map((participant) => participant.jid); - const { id } = await this.client.groupCreate(create.subject, participants); - this.logger.verbose('Group created: ' + id); - - if (create?.description) { - this.logger.verbose('Updating group description: ' + create.description); - await this.client.groupUpdateDescription(id, create.description); - } - - if (create?.promoteParticipants) { - this.logger.verbose('Prometing group participants: ' + participants); - await this.updateGParticipant({ - groupJid: id, - action: 'promote', - participants: participants, - }); - } - - this.logger.verbose('Getting group metadata'); - const group = await this.client.groupMetadata(id); - - return group; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); - } - } - - public async updateGroupPicture(picture: GroupPictureDto) { - this.logger.verbose('Updating group picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture.image)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture.image}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture.image)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture.image, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(picture.groupJid, pic); - this.logger.verbose('Group picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error update group picture', error.toString()); - } - } - - public async updateGroupSubject(data: GroupSubjectDto) { - this.logger.verbose('Updating group subject to: ' + data.subject); - try { - await this.client.groupUpdateSubject(data.groupJid, data.subject); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group subject', error.toString()); - } - } - - public async updateGroupDescription(data: GroupDescriptionDto) { - this.logger.verbose('Updating group description to: ' + data.description); - try { - await this.client.groupUpdateDescription(data.groupJid, data.description); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group description', error.toString()); - } - } - - public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { - this.logger.verbose('Fetching group'); - try { - return await this.client.groupMetadata(id.groupJid); - } catch (error) { - if (reply === 'inner') { - return; - } - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async fetchAllGroups(getParticipants: GetParticipant) { - this.logger.verbose('Fetching all groups'); - try { - const fetch = Object.values(await this.client.groupFetchAllParticipating()); - - const groups = fetch.map((group) => { - const result = { - id: group.id, - subject: group.subject, - subjectOwner: group.subjectOwner, - subjectTime: group.subjectTime, - size: group.participants.length, - creation: group.creation, - owner: group.owner, - desc: group.desc, - descId: group.descId, - restrict: group.restrict, - announce: group.announce, - }; - - if (getParticipants.getParticipants == 'true') { - result['participants'] = group.participants; - } - - return result; - }); - - return groups; - } catch (error) { - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async inviteCode(id: GroupJid) { - this.logger.verbose('Fetching invite code for group: ' + id.groupJid); - try { - const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { - throw new NotFoundException('No invite code', error.toString()); - } - } - - public async inviteInfo(id: GroupInvite) { - this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); - try { - return await this.client.groupGetInviteInfo(id.inviteCode); - } catch (error) { - throw new NotFoundException('No invite info', id.inviteCode); - } - } - - public async sendInvite(id: GroupSendInvite) { - this.logger.verbose('Sending invite for group: ' + id.groupJid); - try { - const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); - this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); - - const inviteUrl = inviteCode.inviteUrl; - this.logger.verbose('Invite url: ' + inviteUrl); - - const numbers = id.numbers.map((number) => this.createJid(number)); - const description = id.description ?? ''; - - const msg = `${description}\n\n${inviteUrl}`; - - const message = { - conversation: msg, - }; - - for await (const number of numbers) { - await this.sendMessageWithTyping(number, message); - } - - this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - - return { send: true, inviteUrl }; - } catch (error) { - throw new NotFoundException('No send invite'); - } - } - - public async revokeInviteCode(id: GroupJid) { - this.logger.verbose('Revoking invite code for group: ' + id.groupJid); - try { - const inviteCode = await this.client.groupRevokeInvite(id.groupJid); - return { revoked: true, inviteCode }; - } catch (error) { - throw new NotFoundException('Revoke error', error.toString()); - } - } - - public async findParticipants(id: GroupJid) { - this.logger.verbose('Fetching participants for group: ' + id.groupJid); - try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; - return { participants }; - } catch (error) { - throw new NotFoundException('No participants', error.toString()); - } - } - - public async updateGParticipant(update: GroupUpdateParticipantDto) { - this.logger.verbose('Updating participants'); - try { - const participants = update.participants.map((p) => this.createJid(p)); - const updateParticipants = await this.client.groupParticipantsUpdate( - update.groupJid, - participants, - update.action, - ); - return { updateParticipants: updateParticipants }; - } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); - } - } - - public async updateGSetting(update: GroupUpdateSettingDto) { - this.logger.verbose('Updating setting for group: ' + update.groupJid); - try { - const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); - return { updateSetting: updateSetting }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async toggleEphemeral(update: GroupToggleEphemeralDto) { - this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); - try { - await this.client.groupToggleEphemeral(update.groupJid, update.expiration); - return { success: true }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async leaveGroup(id: GroupJid) { - this.logger.verbose('Leaving group: ' + id.groupJid); - try { - await this.client.groupLeave(id.groupJid); - return { groupJid: id.groupJid, leave: true }; - } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); - } - } -} +} \ No newline at end of file diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 5adf9ca2..cc1f8da8 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -45,6 +45,9 @@ export declare namespace wa { wuid?: string; profileName?: string; profilePictureUrl?: string; + integration?: string; + number?: string; + token?: string; }; export type LocalWebHook = { @@ -139,3 +142,9 @@ export const MessageSubtype = [ 'viewOnceMessage', 'viewOnceMessageV2', ]; + +export enum Integration { + WHATSAPP_BUSINESS='WHATSAPP-BUSINESS', + WHATSAPP_BAILEYS='WHATSAPP-BAILEYS', + WABussinessService = 'WABussinessService', +} diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index e89d4f56..407ffba5 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -58,6 +58,11 @@ import { SqsService } from './services/sqs.service'; import { TypebotService } from './services/typebot.service'; import { WebhookService } from './services/webhook.service'; import { WebsocketService } from './services/websocket.service'; +import { WABaileysService } from './services/whatsapp.baileys.service'; +import { WAStartupService } from './services/whatsapp.service'; +import { WABussinessService } from './services/whatsapp.business.service'; +import { ConfigService, } from '../config/env.config'; +import EventEmitter2 from 'eventemitter2'; const logger = new Logger('WA MODULE'); @@ -107,7 +112,7 @@ export const typebotController = new TypebotController(typebotService); const webhookService = new WebhookService(waMonitor); -export const webhookController = new WebhookController(webhookService); +export const webhookController = new WebhookController(webhookService, waMonitor); const websocketService = new WebsocketService(waMonitor); @@ -157,4 +162,17 @@ export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); export const groupController = new GroupController(waMonitor); + +export const WAStartupClass: { + [key: string]: new ( + configService: ConfigService, + eventEmitter: EventEmitter2, + repository: RepositoryBroker, + cache: RedisCache, + ) => WAStartupService +} = { + 'WHATSAPP-BUSINESS': WABussinessService, + 'WHATSAPP-BAILEYS': WABaileysService, + }; + logger.info('Module - ON');