feat: whatsapp cloud api

This commit is contained in:
Davidson Gomes 2024-02-17 17:42:49 -03:00
parent 3a37fd9d32
commit 0525501b87
24 changed files with 4768 additions and 3231 deletions

View File

@ -51,6 +51,11 @@ RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
WEBSOCKET_ENABLED=false
WA_BUSINESS_TOKEN_WEBHOOK=evolution
WA_BUSINESS_URL=https://graph.facebook.com
WA_BUSINESS_VERSION=v18.0
WA_BUSINESS_LANGUAGE=pt_BR
SQS_ENABLED=false
SQS_ACCESS_KEY_ID=
SQS_SECRET_ACCESS_KEY=

View File

@ -66,6 +66,11 @@ ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
ENV WEBSOCKET_ENABLED=false
ENV WA_BUSINESS_TOKEN_WEBHOOK=evolution
ENV WA_BUSINESS_URL=https://graph.facebook.com
ENV WA_BUSINESS_VERSION=v18.0
ENV WA_BUSINESS_LANGUAGE=pt_BR
ENV SQS_ENABLED=false
ENV SQS_ACCESS_KEY_ID=
ENV SQS_SECRET_ACCESS_KEY=

View File

@ -86,6 +86,13 @@ export type Websocket = {
ENABLED: boolean;
};
export type WaBusiness = {
TOKEN_WEBHOOK: string;
URL: string;
VERSION: string;
LANGUAGE: string;
};
export type EventsWebhook = {
APPLICATION_STARTUP: boolean;
INSTANCE_CREATE: boolean;
@ -179,6 +186,7 @@ export interface Env {
RABBITMQ: Rabbitmq;
SQS: Sqs;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
LOG: Log;
DEL_INSTANCE: DelInstance;
LANGUAGE: Language;
@ -286,6 +294,12 @@ export class ConfigService {
WEBSOCKET: {
ENABLED: process.env?.WEBSOCKET_ENABLED === 'true',
},
WA_BUSINESS: {
TOKEN_WEBHOOK: process.env.WA_BUSINESS_TOKEN_WEBHOOK || '',
URL: process.env.WA_BUSINESS_URL || '',
VERSION: process.env.WA_BUSINESS_VERSION || '',
LANGUAGE: process.env.WA_BUSINESS_LANGUAGE || 'en',
},
LOG: {
LEVEL: (process.env?.LOG_LEVEL.split(',') as LogLevel[]) || [
'ERROR',

View File

@ -12,7 +12,6 @@ SERVER:
DISABLE_MANAGER: false
DISABLE_DOCS: false
CORS:
ORIGIN:
- "*"
@ -96,6 +95,12 @@ SQS:
WEBSOCKET:
ENABLED: false
WA_BUSINESS:
TOKEN_WEBHOOK: evolution
URL: https://graph.facebook.com
VERSION: v18.0
LANGUAGE: pt_BR
# Global Webhook Settings
# Each instance's Webhook URL and events will be requested at the time it is created
WEBHOOK:
@ -152,12 +157,12 @@ QRCODE:
COLOR: "#198754"
TYPEBOT:
API_VERSION: 'old' # old | latest
API_VERSION: "old" # old | latest
KEEP_OPEN: false
CHATWOOT:
# If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot.
MESSAGE_DELETE: true # false | true
MESSAGE_DELETE: true # false | true
IMPORT:
# This db connection is used to import messages from whatsapp to chatwoot database
DATABASE:
@ -192,5 +197,4 @@ AUTHENTICATION:
EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires
SECRET: L=0YWt]b2w[WF>#>:&E`
LANGUAGE: "pt-BR" # pt-BR, en
LANGUAGE: "pt-BR" # pt-BR, en

View File

@ -277,6 +277,25 @@ export const audioMessageSchema: JSONSchema7 = {
required: ['audioMessage', 'number'],
};
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 buttonMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',

View File

@ -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, WaBusiness } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { BadRequestException, InternalServerErrorException } from '../../exceptions';
import { RedisCache } from '../../libs/redis.client';
@ -12,6 +12,7 @@ import { RepositoryBroker } from '../repository/repository.manager';
import { AuthService, OldToken } from '../services/auth.service';
import { CacheService } from '../services/cache.service';
import { ChatwootService } from '../services/chatwoot.service';
import { IntegrationService } from '../services/integration.service';
import { WAMonitoringService } from '../services/monitor.service';
import { RabbitmqService } from '../services/rabbitmq.service';
import { SettingsService } from '../services/settings.service';
@ -19,8 +20,9 @@ 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 { BaileysStartupService } from '../services/whatsapp.baileys.service';
import { BusinessStartupService } from '../services/whatsapp.business.service';
import { Events, Integration, wa } from '../types/wa.types';
export class InstanceController {
constructor(
@ -36,6 +38,7 @@ export class InstanceController {
private readonly rabbitmqService: RabbitmqService,
private readonly sqsService: SqsService,
private readonly typebotService: TypebotService,
private readonly integrationService: IntegrationService,
private readonly cache: RedisCache,
private readonly chatwootCache: CacheService,
) {}
@ -50,6 +53,7 @@ export class InstanceController {
events,
qrcode,
number,
integration,
token,
chatwoot_account_id,
chatwoot_token,
@ -87,14 +91,31 @@ export class InstanceController {
this.logger.verbose('checking duplicate token');
await this.authService.checkDuplicateToken(token);
if (!token && integration !== Integration.WHATSAPP_BUSINESS) {
throw new BadRequestException('token is required');
}
this.logger.verbose('creating instance');
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
let instance: BaileysStartupService | BusinessStartupService;
if (integration === Integration.WHATSAPP_BUSINESS) {
instance = new BusinessStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
await this.waMonitor.saveInstance({ integration, instanceName, token, number });
} else {
instance = new BaileysStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
}
instance.instanceName = instanceName;
const instanceId = v4();
@ -361,6 +382,23 @@ export class InstanceController {
this.settingsService.create(instance, settings);
let webhook_wa_business = null,
access_token_wa_business = '';
if (integration === Integration.WHATSAPP_BUSINESS) {
if (!number) {
throw new BadRequestException('number is required');
}
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
webhook_wa_business = `${urlServer}/webhook/whatsapp/${encodeURIComponent(instance.instanceName)}`;
access_token_wa_business = this.configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK;
}
this.integrationService.create(instance, {
integration,
number,
token,
});
if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) {
let getQrcode: wa.QrCode;
@ -375,6 +413,9 @@ export class InstanceController {
instance: {
instanceName: instance.instanceName,
instanceId: instanceId,
integration: integration,
webhook_wa_business,
access_token_wa_business,
status: 'created',
},
hash,
@ -470,6 +511,9 @@ export class InstanceController {
instance: {
instanceName: instance.instanceName,
instanceId: instanceId,
integration: integration,
webhook_wa_business,
access_token_wa_business,
status: 'created',
},
hash,

View File

@ -14,6 +14,7 @@ import {
SendReactionDto,
SendStatusDto,
SendStickerDto,
SendTemplateDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { WAMonitoringService } from '../services/monitor.service';
@ -28,6 +29,11 @@ export class SendMessageController {
return await this.waMonitor.waInstances[instanceName].textMessage(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 sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) {
logger.verbose('requested sendMedia from ' + instanceName + ' instance');

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export class IntegrationDto {
integration: string;
number: string;
token: string;
}

View File

@ -142,6 +142,15 @@ export class ContactMessage {
email?: string;
url?: string;
}
export class TemplateMessage {
name: string;
language: string;
}
export class SendTemplateDto extends Metadata {
templateMessage: TemplateMessage;
}
export class SendContactDto extends Metadata {
contactMessage: ContactMessage[];
}

View File

@ -3,6 +3,7 @@ export * from './chamaai.model';
export * from './chat.model';
export * from './chatwoot.model';
export * from './contact.model';
export * from './integration.model';
export * from './label.model';
export * from './message.model';
export * from './proxy.model';

View File

@ -0,0 +1,20 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../libs/db.connect';
export class IntegrationRaw {
_id?: string;
integration?: string;
number?: string;
token?: string;
}
const sqsSchema = new Schema<IntegrationRaw>({
_id: { type: String, _id: true },
integration: { type: String, required: true },
number: { type: String, required: true },
token: { type: String, required: true },
});
export const IntegrationModel = dbserver?.model(IntegrationRaw.name, sqsSchema, 'integration');
export type IntegrationModel = typeof IntegrationModel;

View File

@ -0,0 +1,64 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { ConfigService } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { IntegrationModel, IntegrationRaw } from '../models';
export class IntegrationRepository extends Repository {
constructor(private readonly integrationModel: IntegrationModel, private readonly configService: ConfigService) {
super(configService);
}
private readonly logger = new Logger('IntegrationRepository');
public async create(data: IntegrationRaw, instance: string): Promise<IInsert> {
try {
this.logger.verbose('creating integration');
if (this.dbSettings.ENABLED) {
this.logger.verbose('saving integration to db');
const insert = await this.integrationModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
this.logger.verbose('integration saved to db: ' + insert.modifiedCount + ' integration');
return { insertCount: insert.modifiedCount };
}
this.logger.verbose('saving integration to store');
this.writeStore<IntegrationRaw>({
path: join(this.storePath, 'integration'),
fileName: instance,
data,
});
this.logger.verbose(
'integration saved to store in path: ' + join(this.storePath, 'integration') + '/' + instance,
);
this.logger.verbose('integration created');
return { insertCount: 1 };
} catch (error) {
return error;
}
}
public async find(instance: string): Promise<IntegrationRaw> {
try {
this.logger.verbose('finding integration');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding integration in db');
return await this.integrationModel.findOne({ _id: instance });
}
this.logger.verbose('finding integration in store');
return JSON.parse(
readFileSync(join(this.storePath, 'integration', instance + '.json'), {
encoding: 'utf-8',
}),
) as IntegrationRaw;
} catch (error) {
return {};
}
}
}

View File

@ -9,6 +9,7 @@ import { ChamaaiRepository } from './chamaai.repository';
import { ChatRepository } from './chat.repository';
import { ChatwootRepository } from './chatwoot.repository';
import { ContactRepository } from './contact.repository';
import { IntegrationRepository } from './integration.repository';
import { LabelRepository } from './label.repository';
import { MessageRepository } from './message.repository';
import { MessageUpRepository } from './messageUp.repository';
@ -34,6 +35,7 @@ export class RepositoryBroker {
public readonly typebot: TypebotRepository,
public readonly proxy: ProxyRepository,
public readonly chamaai: ChamaaiRepository,
public readonly integration: IntegrationRepository,
public readonly auth: AuthRepository,
public readonly labels: LabelRepository,
private configService: ConfigService,
@ -71,6 +73,7 @@ export class RepositoryBroker {
const typebotDir = join(storePath, 'typebot');
const proxyDir = join(storePath, 'proxy');
const chamaaiDir = join(storePath, 'chamaai');
const integrationDir = join(storePath, 'integration');
const tempDir = join(storePath, 'temp');
if (!fs.existsSync(authDir)) {
@ -129,6 +132,10 @@ export class RepositoryBroker {
this.logger.verbose('creating chamaai dir: ' + chamaaiDir);
fs.mkdirSync(chamaaiDir, { recursive: true });
}
if (!fs.existsSync(integrationDir)) {
this.logger.verbose('creating integration dir: ' + integrationDir);
fs.mkdirSync(integrationDir, { recursive: true });
}
if (!fs.existsSync(tempDir)) {
this.logger.verbose('creating temp dir: ' + tempDir);
fs.mkdirSync(tempDir, { recursive: true });

View File

@ -12,6 +12,7 @@ import {
reactionMessageSchema,
statusMessageSchema,
stickerMessageSchema,
templateMessageSchema,
textMessageSchema,
} from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router';
@ -26,6 +27,7 @@ import {
SendReactionDto,
SendStatusDto,
SendStickerDto,
SendTemplateDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { sendMessageController } from '../whatsapp.module';
@ -85,6 +87,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<SendTemplateDto>({
request: req,
schema: templateMessageSchema,
ClassRef: SendTemplateDto,
execute: (instance, data) => sendMessageController.sendTemplate(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendButtons'), ...guards, async (req, res) => {
logger.verbose('request received in sendButtons');
logger.verbose('request body: ');

View File

@ -0,0 +1,33 @@
import { Logger } from '../../config/logger.config';
import { InstanceDto } from '../dto/instance.dto';
import { IntegrationDto } from '../dto/integration.dto';
import { IntegrationRaw } from '../models';
import { WAMonitoringService } from './monitor.service';
export class IntegrationService {
constructor(private readonly waMonitor: WAMonitoringService) {}
private readonly logger = new Logger(IntegrationService.name);
public create(instance: InstanceDto, data: IntegrationDto) {
this.logger.verbose('create integration: ' + instance.instanceName);
this.waMonitor.waInstances[instance.instanceName].setIntegration(data);
return { integration: { ...instance, integration: data } };
}
public async find(instance: InstanceDto): Promise<IntegrationRaw> {
try {
this.logger.verbose('find integration: ' + instance.instanceName);
const result = await this.waMonitor.waInstances[instance.instanceName].findIntegration();
if (Object.keys(result).length === 0) {
throw new Error('Integration not found');
}
return result;
} catch (error) {
return { integration: '', number: '', token: '' };
}
}
}

View File

@ -1,6 +1,6 @@
import { execSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { opendirSync, readdirSync, rmSync } from 'fs';
import { existsSync, mkdirSync, opendirSync, readdirSync, rmSync, writeFileSync } from 'fs';
import { Db } from 'mongodb';
import { Collection } from 'mongoose';
import { join } from 'path';
@ -24,8 +24,10 @@ import {
WebsocketModel,
} from '../models';
import { RepositoryBroker } from '../repository/repository.manager';
import { Integration } from '../types/wa.types';
import { CacheService } from './cache.service';
import { WAStartupService } from './whatsapp.service';
import { BaileysStartupService } from './whatsapp.baileys.service';
import { BusinessStartupService } from './whatsapp.business.service';
export class WAMonitoringService {
constructor(
@ -54,7 +56,7 @@ export class WAMonitoringService {
private dbInstance: Db;
private readonly logger = new Logger(WAMonitoringService.name);
public readonly waInstances: Record<string, WAStartupService> = {};
public readonly waInstances: Record<string, BaileysStartupService | BusinessStartupService> = {};
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
@ -64,9 +66,11 @@ export class WAMonitoringService {
setTimeout(async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
if ((await this.waInstances[instance].findIntegration()).integration === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.waInstances[instance]?.removeRabbitmqQueues();
delete this.waInstances[instance];
} else {
@ -353,14 +357,47 @@ export class WAMonitoringService {
}
}
public async saveInstance(data: any) {
this.logger.verbose('Save instance');
try {
const 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);
}
}
private async setInstance(name: string) {
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
const integration = await this.repository.integration.find(name);
let instance: BaileysStartupService | BusinessStartupService;
if (integration.integration === Integration.WHATSAPP_BUSINESS) {
instance = new BusinessStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
} else {
instance = new BaileysStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
}
instance.instanceName = name;
this.logger.verbose('Instance loaded: ' + name);
await instance.connectToWhatsapp();

View File

@ -9,9 +9,9 @@ export class ProxyService {
private readonly logger = new Logger(ProxyService.name);
public create(instance: InstanceDto, data: ProxyDto, reload = true) {
public create(instance: InstanceDto, data: ProxyDto) {
this.logger.verbose('create proxy: ' + instance.instanceName);
this.waMonitor.waInstances[instance.instanceName].setProxy(data, reload);
this.waMonitor.waInstances[instance.instanceName].setProxy(data);
return { proxy: { ...instance, proxy: data } };
}

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

@ -138,6 +138,12 @@ export declare namespace wa {
answerByAudio?: boolean;
};
export type LocalIntegration = {
integration?: string;
number?: string;
token?: string;
};
export type StateConnection = {
instance?: string;
state?: WAConnectionState | 'refused';
@ -155,3 +161,8 @@ export const MessageSubtype = [
'viewOnceMessage',
'viewOnceMessageV2',
];
export const Integration = {
WHATSAPP_BUSINESS: 'WHATSAPP-BUSINESS',
WHATSAPP_BAILEYS: 'WHATSAPP-BAILEYS',
};

View File

@ -24,6 +24,7 @@ import {
ChatModel,
ChatwootModel,
ContactModel,
IntegrationModel,
MessageModel,
MessageUpModel,
ProxyModel,
@ -40,6 +41,7 @@ import { ChamaaiRepository } from './repository/chamaai.repository';
import { ChatRepository } from './repository/chat.repository';
import { ChatwootRepository } from './repository/chatwoot.repository';
import { ContactRepository } from './repository/contact.repository';
import { IntegrationRepository } from './repository/integration.repository';
import { LabelRepository } from './repository/label.repository';
import { MessageRepository } from './repository/message.repository';
import { MessageUpRepository } from './repository/messageUp.repository';
@ -55,6 +57,7 @@ import { AuthService } from './services/auth.service';
import { CacheService } from './services/cache.service';
import { ChamaaiService } from './services/chamaai.service';
import { ChatwootService } from './services/chatwoot.service';
import { IntegrationService } from './services/integration.service';
import { WAMonitoringService } from './services/monitor.service';
import { ProxyService } from './services/proxy.service';
import { RabbitmqService } from './services/rabbitmq.service';
@ -77,6 +80,7 @@ const proxyRepository = new ProxyRepository(ProxyModel, configService);
const chamaaiRepository = new ChamaaiRepository(ChamaaiModel, configService);
const rabbitmqRepository = new RabbitmqRepository(RabbitmqModel, configService);
const sqsRepository = new SqsRepository(SqsModel, configService);
const integrationRepository = new IntegrationRepository(IntegrationModel, configService);
const chatwootRepository = new ChatwootRepository(ChatwootModel, configService);
const settingsRepository = new SettingsRepository(SettingsModel, configService);
const authRepository = new AuthRepository(AuthModel, configService);
@ -96,6 +100,7 @@ export const repository = new RepositoryBroker(
typebotRepository,
proxyRepository,
chamaaiRepository,
integrationRepository,
authRepository,
labelRepository,
configService,
@ -138,6 +143,8 @@ const sqsService = new SqsService(waMonitor);
export const sqsController = new SqsController(sqsService);
const integrationService = new IntegrationService(waMonitor);
const chatwootService = new ChatwootService(waMonitor, configService, repository, chatwootCache);
export const chatwootController = new ChatwootController(chatwootService, configService, repository);
@ -159,6 +166,7 @@ export const instanceController = new InstanceController(
rabbitmqService,
sqsService,
typebotService,
integrationService,
cache,
chatwootCache,
);