added: Integration with whatsapp business api

This commit is contained in:
microprocessgit 2024-01-16 16:19:30 -03:00
parent 1a2546c52b
commit 1686ef58cf
23 changed files with 4406 additions and 2871 deletions

View File

@ -46,7 +46,7 @@
"@figuro/chatwoot-sdk": "^1.1.16", "@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2", "@sentry/node": "^7.59.2",
"@whiskeysockets/baileys": "^6.5.0", "@whiskeysockets/baileys": "github:PurpShell/Baileys#combined",
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"aws-sdk": "^2.1499.0", "aws-sdk": "^2.1499.0",
"axios": "^1.3.5", "axios": "^1.3.5",

View File

@ -120,6 +120,7 @@ export type EventsWebhook = {
export type ApiKey = { KEY: string }; export type ApiKey = { KEY: string };
export type Jwt = { EXPIRIN_IN: number; SECRET: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string };
export type WABussiness = { ACESS_TOKEN: string, URL: string, VERSION: string, LANGUAGE: string };
export type Auth = { export type Auth = {
API_KEY: ApiKey; API_KEY: ApiKey;
@ -161,6 +162,7 @@ export interface Env {
TYPEBOT: Typebot; TYPEBOT: Typebot;
AUTHENTICATION: Auth; AUTHENTICATION: Auth;
PRODUCTION?: Production; PRODUCTION?: Production;
WABUSSINESS: WABussiness;
} }
export type Key = keyof Env; export type Key = keyof Env;
@ -337,6 +339,12 @@ export class ConfigService {
SECRET: process.env.AUTHENTICATION_JWT_SECRET || 'L=0YWt]b2w[WF>#>:&E`', 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
},
}; };
} }
} }

View File

@ -155,7 +155,7 @@ export class Logger {
function salvarLog(env: any, log: string): void { function salvarLog(env: any, log: string): void {
mkdir(env.LOG_PATH, { recursive: true }, (err) => { if (err) throw err; }); 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'; file = env.LOG_PATH + '/' + file + '.txt';
try { try {
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
@ -176,9 +176,9 @@ function excluirArquivosAntigos(path: string, diasLimite: number): void {
data.setDate(data.getDate() - diasLimite); data.setDate(data.getDate() - diasLimite);
fs.readdirSync(path).forEach((nomeArquivo) => { fs.readdirSync(path).forEach((nomeArquivo) => {
let Timerfile = new Date(parseInt(nomeArquivo.substring(8, 4)), 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); fs.unlinkSync(path + '/' + nomeArquivo);
console.log(`Arquivo ${nomeArquivo} excluído.`); console.log(`Arquivo ${nomeArquivo} excluído.`);
} }

View File

@ -5,10 +5,10 @@ const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {}; const properties = {};
propertyNames.forEach( propertyNames.forEach(
(property) => (property) =>
(properties[property] = { (properties[property] = {
minLength: 1, minLength: 1,
description: `The "${property}" cannot be empty`, description: `The "${property}" cannot be empty`,
}), }),
); );
return { return {
if: { if: {
@ -389,6 +389,25 @@ export const listMessageSchema: JSONSchema7 = {
required: ['number', 'listMessage'], 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 = { export const contactMessageSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', 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 = { export const profileSchema: JSONSchema7 = {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -11,6 +11,7 @@ import {
ReadMessageDto, ReadMessageDto,
SendPresenceDto, SendPresenceDto,
WhatsAppNumberDto, WhatsAppNumberDto,
NumberBusiness,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { ContactQuery } from '../repository/contact.repository'; import { ContactQuery } from '../repository/contact.repository';
@ -117,4 +118,9 @@ export class ChatController {
logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); 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);
}
} }

View File

@ -3,7 +3,7 @@ import { isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { v4 } from 'uuid'; 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 { Logger } from '../../config/logger.config';
import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { BadRequestException, InternalServerErrorException } from '../../exceptions';
import { RedisCache } from '../../libs/redis.client'; import { RedisCache } from '../../libs/redis.client';
@ -19,8 +19,8 @@ import { SqsService } from '../services/sqs.service';
import { TypebotService } from '../services/typebot.service'; import { TypebotService } from '../services/typebot.service';
import { WebhookService } from '../services/webhook.service'; import { WebhookService } from '../services/webhook.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
import { WAStartupService } from '../services/whatsapp.service'; import { Events, wa, Integration } from '../types/wa.types';
import { Events, wa } from '../types/wa.types'; import { WAStartupClass } from '../whatsapp.module';
export class InstanceController { export class InstanceController {
constructor( constructor(
@ -50,6 +50,7 @@ export class InstanceController {
events, events,
qrcode, qrcode,
number, number,
integration,
token, token,
chatwoot_account_id, chatwoot_account_id,
chatwoot_token, chatwoot_token,
@ -85,9 +86,10 @@ export class InstanceController {
await this.authService.checkDuplicateToken(token); await this.authService.checkDuplicateToken(token);
this.logger.verbose('creating instance'); 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.instanceName = instanceName;
instance.instanceNumber = number;
instance.instanceToken = token;
const instanceId = v4(); const instanceId = v4();
instance.sendDataWebhook(Events.INSTANCE_CREATE, { instance.sendDataWebhook(Events.INSTANCE_CREATE, {
@ -359,20 +361,30 @@ export class InstanceController {
this.settingsService.create(instance, settings); this.settingsService.create(instance, settings);
const urlServer = this.configService.get<HttpServer>('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>('WABUSSINESS').ACESS_TOKEN;
}
if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) {
let getQrcode: wa.QrCode; let getQrcode: wa.QrCode;
await this.waMonitor.saveInstance({integration, instanceName, token, number});
if (qrcode) { if (qrcode) {
this.logger.verbose('creating qrcode'); this.logger.verbose('creating qrcode');
await instance.connectToWhatsapp(number); await instance.connectToWhatsapp(number);
await delay(5000); await delay(5000);
getQrcode = instance.qrCode; getQrcode = instance.qrCode;
} }
const result = { const result = {
instance: { instance: {
instanceName: instance.instanceName, instanceName: instance.instanceName,
instanceId: instanceId, instanceId: instanceId,
integration: integration,
status: 'created', status: 'created',
}, },
hash, hash,
@ -405,6 +417,7 @@ export class InstanceController {
listening_from_me: typebot_listening_from_me, listening_from_me: typebot_listening_from_me,
}, },
settings, settings,
webhook_url: webhook_url,
qrcode: getQrcode, qrcode: getQrcode,
proxy, proxy,
}; };
@ -423,6 +436,10 @@ export class InstanceController {
throw new BadRequestException('token is required'); throw new BadRequestException('token is required');
} }
if (!integration) {
throw new BadRequestException('integration is required');
}
if (!chatwoot_url) { if (!chatwoot_url) {
throw new BadRequestException('url is required'); throw new BadRequestException('url is required');
} }
@ -443,8 +460,6 @@ export class InstanceController {
throw new BadRequestException('conversation_pending is required'); throw new BadRequestException('conversation_pending is required');
} }
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
try { try {
this.chatwootService.create(instance, { this.chatwootService.create(instance, {
enabled: true, enabled: true,
@ -617,7 +632,7 @@ export class InstanceController {
await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName);
this.logger.verbose('close connection 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' } }; return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
} catch (error) { } catch (error) {

View File

@ -15,6 +15,7 @@ import {
SendStatusDto, SendStatusDto,
SendStickerDto, SendStickerDto,
SendTextDto, SendTextDto,
SendTemplateDto,
} from '../dto/sendMessage.dto'; } from '../dto/sendMessage.dto';
import { WAMonitoringService } from '../services/monitor.service'; import { WAMonitoringService } from '../services/monitor.service';
@ -86,6 +87,11 @@ export class SendMessageController {
return await this.waMonitor.waInstances[instanceName].listMessage(data); 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) { public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) {
logger.verbose('requested sendContact from ' + instanceName + ' instance'); logger.verbose('requested sendContact from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].contactMessage(data); return await this.waMonitor.waInstances[instanceName].contactMessage(data);

View File

@ -5,11 +5,19 @@ import { BadRequestException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { WebhookDto } from '../dto/webhook.dto'; import { WebhookDto } from '../dto/webhook.dto';
import { WebhookService } from '../services/webhook.service'; 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'); const logger = new Logger('WebhookController');
export class 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) { public async createWebhook(instance: InstanceDto, data: WebhookDto) {
logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance');
@ -61,4 +69,9 @@ export class WebhookController {
logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance');
return this.webhookService.find(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);
}
} }

View File

@ -26,8 +26,11 @@ export class NumberBusiness {
message?: string; message?: string;
description?: string; description?: string;
email?: string; email?: string;
website?: string[]; websites?: string[];
address?: string; address?: string;
about?: string;
vertical?: string;
profilehandle?: string;
} }
export class ProfileNameDto { export class ProfileNameDto {

View File

@ -3,6 +3,7 @@ export class InstanceDto {
instanceId?: string; instanceId?: string;
qrcode?: boolean; qrcode?: boolean;
number?: string; number?: string;
integration?: string;
token?: string; token?: string;
webhook?: string; webhook?: string;
webhook_by_events?: boolean; webhook_by_events?: boolean;

View File

@ -134,6 +134,15 @@ export class SendListDto extends Metadata {
listMessage: ListMessage; listMessage: ListMessage;
} }
export class TemplateMessage {
name: string;
language: string;
}
export class SendTemplateDto extends Metadata {
templateMessage: TemplateMessage;
}
export class ContactMessage { export class ContactMessage {
fullName: string; fullName: string;
wuid: string; wuid: string;

View File

@ -15,6 +15,7 @@ import {
profileStatusSchema, profileStatusSchema,
readMessageSchema, readMessageSchema,
whatsappNumberSchema, whatsappNumberSchema,
profileBusinessSchema,
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router'; import { RouterBroker } from '../abstract/abstract.router';
import { import {
@ -29,6 +30,7 @@ import {
ReadMessageDto, ReadMessageDto,
SendPresenceDto, SendPresenceDto,
WhatsAppNumberDto, WhatsAppNumberDto,
NumberBusiness,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { ContactQuery } from '../repository/contact.repository'; import { ContactQuery } from '../repository/contact.repository';
@ -213,6 +215,23 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response); 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<NumberBusiness>({
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) => { .get(this.routerPath('findChats'), ...guards, async (req, res) => {
logger.verbose('request received in findChats'); logger.verbose('request received in findChats');
logger.verbose('request body: '); logger.verbose('request body: ');
@ -291,7 +310,7 @@ export class ChatRouter extends RouterBroker {
const response = await this.dataValidate<ProfilePictureDto>({ const response = await this.dataValidate<ProfilePictureDto>({
request: req, request: req,
schema: profilePictureSchema, schema: profileBusinessSchema,
ClassRef: ProfilePictureDto, ClassRef: ProfilePictureDto,
execute: (instance, data) => chatController.fetchBusinessProfile(instance, data), execute: (instance, data) => chatController.fetchBusinessProfile(instance, data),
}); });

View File

@ -53,7 +53,7 @@ router
.use('/message', new MessageRouter(...guards).router) .use('/message', new MessageRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router)
.use('/group', new GroupRouter(...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('/chatwoot', new ChatwootRouter(...guards).router)
.use('/settings', new SettingsRouter(...guards).router) .use('/settings', new SettingsRouter(...guards).router)
.use('/websocket', new WebsocketRouter(...guards).router) .use('/websocket', new WebsocketRouter(...guards).router)

View File

@ -13,6 +13,7 @@ import {
statusMessageSchema, statusMessageSchema,
stickerMessageSchema, stickerMessageSchema,
textMessageSchema, textMessageSchema,
templateMessageSchema,
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router'; import { RouterBroker } from '../abstract/abstract.router';
import { import {
@ -27,6 +28,7 @@ import {
SendStatusDto, SendStatusDto,
SendStickerDto, SendStickerDto,
SendTextDto, SendTextDto,
SendTemplateDto,
} from '../dto/sendMessage.dto'; } from '../dto/sendMessage.dto';
import { sendMessageController } from '../whatsapp.module'; import { sendMessageController } from '../whatsapp.module';
import { HttpStatus } from './index.router'; import { HttpStatus } from './index.router';
@ -133,6 +135,22 @@ export class MessageRouter extends RouterBroker {
return res.status(HttpStatus.CREATED).json(response); 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<SendTemplateDto>({
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) => { .post(this.routerPath('sendContact'), ...guards, async (req, res) => {
logger.verbose('request received in sendContact'); logger.verbose('request received in sendContact');
logger.verbose('request body: '); logger.verbose('request body: ');

View File

@ -7,11 +7,12 @@ import { InstanceDto } from '../dto/instance.dto';
import { WebhookDto } from '../dto/webhook.dto'; import { WebhookDto } from '../dto/webhook.dto';
import { webhookController } from '../whatsapp.module'; import { webhookController } from '../whatsapp.module';
import { HttpStatus } from './index.router'; import { HttpStatus } from './index.router';
import { WABussiness, ConfigService } from '../../config/env.config';
const logger = new Logger('WebhookRouter'); const logger = new Logger('WebhookRouter');
export class WebhookRouter extends RouterBroker { export class WebhookRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) { constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
super(); super();
this.router this.router
.post(this.routerPath('set'), ...guards, async (req, res) => { .post(this.routerPath('set'), ...guards, async (req, res) => {
@ -30,6 +31,32 @@ export class WebhookRouter extends RouterBroker {
res.status(HttpStatus.CREATED).json(response); 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<InstanceDto>({
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>('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) => { .get(this.routerPath('find'), ...guards, async (req, res) => {
logger.verbose('request received in findWebhook'); logger.verbose('request received in findWebhook');
logger.verbose('request body: '); logger.verbose('request body: ');

View File

@ -6,12 +6,12 @@ import Jimp from 'jimp';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; 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 { Logger } from '../../config/logger.config';
import { ROOT_DIR } from '../../config/path.config'; import { ROOT_DIR } from '../../config/path.config';
import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.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 { MessageRaw } from '../models';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { Events } from '../types/wa.types'; import { Events } from '../types/wa.types';
@ -457,7 +457,7 @@ export class ChatwootService {
if (!findParticipant.name || findParticipant.name === chatId) { if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, { await this.updateContact(instance, findParticipant.id, {
name: body.pushName, name: body.pushName,
avatar_url: picture_url.profilePictureUrl || null, avatar_url: picture_url?.profilePictureUrl || null,
}); });
} }
} else { } else {
@ -467,7 +467,7 @@ export class ChatwootService {
filterInbox.id, filterInbox.id,
false, false,
body.pushName, body.pushName,
picture_url.profilePictureUrl || null, picture_url?.profilePictureUrl || null,
body.key.participant, body.key.participant,
); );
} }
@ -483,7 +483,7 @@ export class ChatwootService {
if (body.key.fromMe) { if (body.key.fromMe) {
if (findContact) { if (findContact) {
contact = await this.updateContact(instance, findContact.id, { contact = await this.updateContact(instance, findContact.id, {
avatar_url: picture_url.profilePictureUrl || null, avatar_url: picture_url?.profilePictureUrl || null,
}); });
} else { } else {
const jid = isGroup ? null : body.key.remoteJid; const jid = isGroup ? null : body.key.remoteJid;
@ -493,7 +493,7 @@ export class ChatwootService {
filterInbox.id, filterInbox.id,
isGroup, isGroup,
nameContact, nameContact,
picture_url.profilePictureUrl || null, picture_url?.profilePictureUrl || null,
jid, jid,
); );
} }
@ -502,11 +502,11 @@ export class ChatwootService {
if (!findContact.name || findContact.name === chatId) { if (!findContact.name || findContact.name === chatId) {
contact = await this.updateContact(instance, findContact.id, { contact = await this.updateContact(instance, findContact.id, {
name: nameContact, name: nameContact,
avatar_url: picture_url.profilePictureUrl || null, avatar_url: picture_url?.profilePictureUrl || null,
}); });
} else { } else {
contact = await this.updateContact(instance, findContact.id, { contact = await this.updateContact(instance, findContact.id, {
avatar_url: picture_url.profilePictureUrl || null, avatar_url: picture_url?.profilePictureUrl || null,
}); });
} }
if (!contact) { if (!contact) {
@ -520,7 +520,7 @@ export class ChatwootService {
filterInbox.id, filterInbox.id,
isGroup, isGroup,
nameContact, nameContact,
picture_url.profilePictureUrl || null, picture_url?.profilePictureUrl || null,
jid, jid,
); );
} }
@ -785,9 +785,9 @@ export class ChatwootService {
const replyToIds = await this.getReplyToIds(messageBody, instance); const replyToIds = await this.getReplyToIds(messageBody, instance);
if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) {
data.append('content_attributes', { data.append('content_attributes',
...replyToIds, JSON.stringify(replyToIds),
}); );
} }
} }
@ -1023,10 +1023,10 @@ export class ChatwootService {
// Chatwoot to Whatsapp // Chatwoot to Whatsapp
const messageReceived = body.content const messageReceived = body.content
? body.content ? body.content
.replaceAll(/(?<!\*)\*((?!\s)([^\n*]+?)(?<!\s))\*(?!\*)/g, '_$1_') // Substitui * por _ .replaceAll(/(?<!\*)\*((?!\s)([^\n*]+?)(?<!\s))\*(?!\*)/g, '_$1_') // Substitui * por _
.replaceAll(/\*{2}((?!\s)([^\n*]+?)(?<!\s))\*{2}/g, '*$1*') // Substitui ** por * .replaceAll(/\*{2}((?!\s)([^\n*]+?)(?<!\s))\*{2}/g, '*$1*') // Substitui ** por *
.replaceAll(/~{2}((?!\s)([^\n*]+?)(?<!\s))~{2}/g, '~$1~') // Substitui ~~ por ~ .replaceAll(/~{2}((?!\s)([^\n*]+?)(?<!\s))~{2}/g, '~$1~') // Substitui ~~ por ~
.replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ``` .replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ```
: body.content; : body.content;
const senderName = body?.sender?.available_name || body?.sender?.name; const senderName = body?.sender?.available_name || body?.sender?.name;
@ -1044,7 +1044,7 @@ export class ChatwootService {
limit: 1, limit: 1,
}); });
if (message.length && message[0].key?.id) { if (message.length && message[0].key?.id) {
await waInstance?.client.sendMessage(message[0].key.remoteJid, { delete: message[0].key }); await waInstance?.client?.sendMessage(message[0].key.remoteJid, { delete: message[0].key });
} }
return { message: 'bot' }; return { message: 'bot' };
} }
@ -1123,6 +1123,36 @@ export class ChatwootService {
this.logger.verbose('Format message to send'); this.logger.verbose('Format message to send');
let formatText: string; let formatText: string;
const regex = /^▶️.*◀️$/;
if (regex.test(messageReceived)) {
const data: SendTemplateDto = {
number: chatId,
templateMessage: {
name: messageReceived.replace(/[^\x20-\x7E]/g, ''),
language: this.configService.get<WABussiness>('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) { if (senderName === null || senderName === undefined) {
formatText = messageReceived; formatText = messageReceived;
} else { } else {
@ -1137,7 +1167,7 @@ export class ChatwootService {
for (const message of body.conversation.messages) { for (const message of body.conversation.messages) {
this.logger.verbose('check if message is media'); 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'); this.logger.verbose('message is media');
for (const attachment of message.attachments) { for (const attachment of message.attachments) {
this.logger.verbose('send media to whatsapp'); this.logger.verbose('send media to whatsapp');
@ -1513,9 +1543,9 @@ export class ChatwootService {
const originalMessage = await this.getConversationMessage(body.message); const originalMessage = await this.getConversationMessage(body.message);
const bodyMessage = originalMessage const bodyMessage = originalMessage
? originalMessage ? originalMessage
.replaceAll(/\*((?!\s)([^\n*]+?)(?<!\s))\*/g, '**$1**') .replaceAll(/\*((?!\s)([^\n*]+?)(?<!\s))\*/g, '**$1**')
.replaceAll(/_((?!\s)([^\n_]+?)(?<!\s))_/g, '*$1*') .replaceAll(/_((?!\s)([^\n_]+?)(?<!\s))_/g, '*$1*')
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~') .replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage; : originalMessage;
this.logger.verbose('body message: ' + bodyMessage); this.logger.verbose('body message: ' + bodyMessage);

View File

@ -1,6 +1,6 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { opendirSync, readdirSync, rmSync } from 'fs'; import { opendirSync, readdirSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { Db } from 'mongodb'; import { Db } from 'mongodb';
import { join } from 'path'; import { join } from 'path';
@ -27,6 +27,7 @@ import {
} from '../models'; } from '../models';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { WAStartupService } from './whatsapp.service'; import { WAStartupService } from './whatsapp.service';
import { WAStartupClass } from '../whatsapp.module';
export class WAMonitoringService { export class WAMonitoringService {
constructor( constructor(
@ -359,13 +360,21 @@ export class WAMonitoringService {
} }
private async setInstance(name: string) { private async setInstance(name: string) {
const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); const path = join(INSTANCE_DIR, name);
let values: any;
if(this.db.ENABLED )
values = await this.dbInstance.collection(name).findOne({ _id: 'integration' })
else
values = JSON.parse(readFileSync(path + '/integration.json', 'utf8'));
const instance = new WAStartupClass[values.integration]
(this.configService, this.eventEmitter, this.repository, this.cache);
instance.instanceName = name; instance.instanceName = name;
instance.instanceNumber = values.number;
instance.instanceToken = values.token;
this.waInstances[name] = instance;
this.logger.verbose('Instance loaded: ' + name); this.logger.verbose('Instance loaded: ' + name);
await instance.connectToWhatsapp(); await instance.connectToWhatsapp();
this.logger.verbose('connectToWhatsapp: ' + name); this.logger.verbose('connectToWhatsapp: ' + name);
this.waInstances[name] = instance;
} }
private async loadInstancesFromRedis() { private async loadInstancesFromRedis() {
@ -473,4 +482,27 @@ export class WAMonitoringService {
} }
}); });
} }
public async saveInstance(data: any) {
this.logger.verbose('Save instance');
try {
let msgParsed = JSON.parse(JSON.stringify(data));
if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
await this.repository.dbServer.connect();
await this.dbInstance.collection(data.instanceName).replaceOne({ _id: 'integration' }, msgParsed,
{
upsert: true,
});
} else {
const path = join(INSTANCE_DIR, data.instanceName)
if (!existsSync(path))
mkdirSync(path, { recursive: true });
writeFileSync(path + '/integration.json', JSON.stringify(msgParsed));
}
} catch (error) {
this.logger.error(error);
}
}
} }

View File

@ -6,7 +6,7 @@ import { Logger } from '../../config/logger.config';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { Session, TypebotDto } from '../dto/typebot.dto'; import { Session, TypebotDto } from '../dto/typebot.dto';
import { MessageRaw } from '../models'; import { MessageRaw } from '../models';
import { Events } from '../types/wa.types'; import { Events, Integration } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service'; import { WAMonitoringService } from './monitor.service';
export class TypebotService { export class TypebotService {
@ -405,6 +405,7 @@ export class TypebotService {
} }
async function processMessages(instance, messages, input, clientSideActions, eventEmitter) { async function processMessages(instance, messages, input, clientSideActions, eventEmitter) {
let qtdMessages = 0, buttonText = '';
for (const message of messages) { for (const message of messages) {
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
@ -463,20 +464,24 @@ export class TypebotService {
} }
formattedText = formattedText.replace(/\n$/, ''); formattedText = formattedText.replace(/\n$/, '');
qtdMessages++;
await instance.textMessage({ if (instance?.constructor.name == Integration.WABussinessService &&
number: remoteJid.split('@')[0], input?.type === 'choice input' && messages.length == qtdMessages) {
options: { buttonText = formattedText;
delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000, } else {
presence: 'composing', await instance.textMessage({
linkPreview: linkPreview, number: remoteJid.split('@')[0],
}, options: {
textMessage: { delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000,
text: formattedText, presence: 'composing',
}, linkPreview: linkPreview,
}); },
textMessage: {
text: formattedText,
},
});
}
} }
if (message.type === 'image') { if (message.type === 'image') {
await instance.mediaMessage({ await instance.mediaMessage({
number: remoteJid.split('@')[0], number: remoteJid.split('@')[0],
@ -522,27 +527,49 @@ export class TypebotService {
if (input) { if (input) {
if (input.type === 'choice input') { if (input.type === 'choice input') {
let formattedText = '';
const items = input.items; const items = input.items;
if (instance?.constructor.name == Integration.WABussinessService) {
let buttons = [];
for (const item of items) {
buttons.push({
buttonId: item.id,
buttonText: item.content,
});
}
await instance.buttonMessage({
number: remoteJid.split('@')[0],
options: {
delay: 1200,
presence: 'composing',
linkPreview: false,
},
buttonMessage: {
title: buttonText,
buttons,
},
});
for (const item of items) { } else {
formattedText += `▶️ ${item.content}\n`; let formattedText = '';
for (const item of items) {
formattedText += `▶️ ${item.content}\n`;
}
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage({
number: remoteJid.split('@')[0],
options: {
delay: 1200,
presence: 'composing',
linkPreview: false,
},
textMessage: {
text: formattedText,
},
});
} }
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage({
number: remoteJid.split('@')[0],
options: {
delay: 1200,
presence: 'composing',
linkPreview: false,
},
textMessage: {
text: formattedText,
},
});
} }
} else { } else {
eventEmitter.emit('typebot:end', { eventEmitter.emit('typebot:end', {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,9 @@ export declare namespace wa {
wuid?: string; wuid?: string;
profileName?: string; profileName?: string;
profilePictureUrl?: string; profilePictureUrl?: string;
integration?: string;
number?: string;
token?: string;
}; };
export type LocalWebHook = { export type LocalWebHook = {
@ -139,3 +142,9 @@ export const MessageSubtype = [
'viewOnceMessage', 'viewOnceMessage',
'viewOnceMessageV2', 'viewOnceMessageV2',
]; ];
export enum Integration {
WHATSAPP_BUSINESS='WHATSAPP-BUSINESS',
WHATSAPP_BAILEYS='WHATSAPP-BAILEYS',
WABussinessService = 'WABussinessService',
}

View File

@ -58,6 +58,11 @@ import { SqsService } from './services/sqs.service';
import { TypebotService } from './services/typebot.service'; import { TypebotService } from './services/typebot.service';
import { WebhookService } from './services/webhook.service'; import { WebhookService } from './services/webhook.service';
import { WebsocketService } from './services/websocket.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'); const logger = new Logger('WA MODULE');
@ -107,7 +112,7 @@ export const typebotController = new TypebotController(typebotService);
const webhookService = new WebhookService(waMonitor); const webhookService = new WebhookService(waMonitor);
export const webhookController = new WebhookController(webhookService); export const webhookController = new WebhookController(webhookService, waMonitor);
const websocketService = new WebsocketService(waMonitor); const websocketService = new WebsocketService(waMonitor);
@ -157,4 +162,17 @@ export const sendMessageController = new SendMessageController(waMonitor);
export const chatController = new ChatController(waMonitor); export const chatController = new ChatController(waMonitor);
export const groupController = new GroupController(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'); logger.info('Module - ON');