From 26bddf3c53f3db9231da9df575dc9b966126a446 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 12 Jul 2024 12:32:44 -0300 Subject: [PATCH] Add support for managing WhatsApp templates via official API This commit introduces changes to support managing WhatsApp templates using the official WhatsApp Business API. The following modifications have been made: - Implemented a new Template model in the Prisma schema, including fields for template ID, name, language, and associated Instance (business ID, instance ID, and created/updated timestamps). - Modified the Instance model in the Prisma schema to include a Template relationship. - Updated InstanceController to include a new `businessId` property in the InstanceDto. - Added a new TemplateRouter, TemplateController, and TemplateService to handle template-related requests and services. - Updated the WebhookService to utilize the new TemplateService. - Added new TypebotController, WebhookController, and WAMonitoringService methods to handle template-related events. - Updated the validate schema to include a new template schema. The main goal of this commit is to enable managing WhatsApp templates, including creating, updating, and deleting templates, as well as associating them with specific instances. --- .../migration.sql | 21 ++++ prisma/postgresql-schema.prisma | 12 ++ src/api/controllers/instance.controller.ts | 1 + src/api/controllers/template.controller.ts | 15 +++ src/api/dto/template.dto.ts | 7 ++ src/api/routes/index.router.ts | 2 + src/api/routes/template.router.ts | 38 +++++++ src/api/server.module.ts | 5 + src/api/services/channel.service.ts | 4 +- .../channels/whatsapp.business.service.ts | 9 +- src/api/services/monitor.service.ts | 5 + src/api/services/template.service.ts | 105 ++++++++++++++++++ src/api/types/wa.types.ts | 1 + src/validate/template.schema.ts | 35 ++++++ src/validate/validate.schema.ts | 1 + 15 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20240712150256_create_templates_table/migration.sql create mode 100644 src/api/controllers/template.controller.ts create mode 100644 src/api/dto/template.dto.ts create mode 100644 src/api/routes/template.router.ts create mode 100644 src/api/services/template.service.ts create mode 100644 src/validate/template.schema.ts diff --git a/prisma/migrations/20240712150256_create_templates_table/migration.sql b/prisma/migrations/20240712150256_create_templates_table/migration.sql new file mode 100644 index 00000000..cb441beb --- /dev/null +++ b/prisma/migrations/20240712150256_create_templates_table/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "Template" ( + "id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "language" VARCHAR(255) NOT NULL, + "templateId" VARCHAR(255) NOT NULL, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL, + "instanceId" TEXT NOT NULL, + + CONSTRAINT "Template_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Template_templateId_key" ON "Template"("templateId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Template_instanceId_key" ON "Template"("instanceId"); + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index fdb73b24..3cffa90c 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -75,6 +75,7 @@ model Instance { MessageUpdate MessageUpdate[] TypebotSession TypebotSession[] TypebotSetting TypebotSetting? + Template Template? } model Session { @@ -305,3 +306,14 @@ model TypebotSetting { Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String @unique } + +model Template { + id String @id @default(cuid()) + name String @db.VarChar(255) + language String @db.VarChar(255) + templateId String @unique @db.VarChar(255) + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String @unique +} diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index 4996f317..8310aa1a 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -134,6 +134,7 @@ export class InstanceController { integration, token: hash, number, + businessId, }); instance.sendDataWebhook(Events.INSTANCE_CREATE, { diff --git a/src/api/controllers/template.controller.ts b/src/api/controllers/template.controller.ts new file mode 100644 index 00000000..b55100c7 --- /dev/null +++ b/src/api/controllers/template.controller.ts @@ -0,0 +1,15 @@ +import { InstanceDto } from '../dto/instance.dto'; +import { TemplateDto } from '../dto/template.dto'; +import { TemplateService } from '../services/template.service'; + +export class TemplateController { + constructor(private readonly templateService: TemplateService) {} + + public async createTemplate(instance: InstanceDto, data: TemplateDto) { + return this.templateService.create(instance, data); + } + + public async findTemplate(instance: InstanceDto) { + return this.templateService.find(instance); + } +} diff --git a/src/api/dto/template.dto.ts b/src/api/dto/template.dto.ts new file mode 100644 index 00000000..1a5f69ff --- /dev/null +++ b/src/api/dto/template.dto.ts @@ -0,0 +1,7 @@ +export class TemplateDto { + name: string; + category: string; + allowCategoryChange: boolean; + language: string; + components: any; +} diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index afffb8e3..9e407eb3 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -17,6 +17,7 @@ import { LabelRouter } from './label.router'; import { ProxyRouter } from './proxy.router'; import { MessageRouter } from './sendMessage.router'; import { SettingsRouter } from './settings.router'; +import { TemplateRouter } from './template.router'; import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; @@ -53,6 +54,7 @@ router .use('/chat', new ChatRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) .use('/webhook', new WebhookRouter(configService, ...guards).router) + .use('/template', new TemplateRouter(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/api/routes/template.router.ts b/src/api/routes/template.router.ts new file mode 100644 index 00000000..8eab843e --- /dev/null +++ b/src/api/routes/template.router.ts @@ -0,0 +1,38 @@ +import { RequestHandler, Router } from 'express'; + +import { ConfigService } from '../../config/env.config'; +import { instanceSchema, templateSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { TemplateDto } from '../dto/template.dto'; +import { templateController } from '../server.module'; +import { HttpStatus } from './index.router'; + +export class TemplateRouter extends RouterBroker { + constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('create'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: templateSchema, + ClassRef: TemplateDto, + execute: (instance, data) => templateController.createTemplate(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => templateController.findTemplate(instance), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 505d2f83..73111e31 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -9,6 +9,7 @@ import { LabelController } from './controllers/label.controller'; import { ProxyController } from './controllers/proxy.controller'; import { SendMessageController } from './controllers/sendMessage.controller'; import { SettingsController } from './controllers/settings.controller'; +import { TemplateController } from './controllers/template.controller'; import { WebhookController } from './controllers/webhook.controller'; import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service'; @@ -27,6 +28,7 @@ import { CacheService } from './services/cache.service'; import { WAMonitoringService } from './services/monitor.service'; import { ProxyService } from './services/proxy.service'; import { SettingsService } from './services/settings.service'; +import { TemplateService } from './services/template.service'; import { WebhookService } from './services/webhook.service'; const logger = new Logger('WA MODULE'); @@ -64,6 +66,9 @@ export const typebotController = new TypebotController(typebotService); const webhookService = new WebhookService(waMonitor, prismaRepository); export const webhookController = new WebhookController(webhookService, waMonitor); +const templateService = new TemplateService(waMonitor, prismaRepository, configService); +export const templateController = new TemplateController(templateService); + const websocketService = new WebsocketService(waMonitor); export const websocketController = new WebsocketController(websocketService); diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 13f3a666..aa095607 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -69,12 +69,14 @@ export class ChannelStartupService { public typebotService = new TypebotService(waMonitor, this.configService, this.prismaRepository); public setInstance(instance: InstanceDto) { - this.instance.name = instance.instanceName; this.logger.setInstance(instance.instanceName); + + this.instance.name = instance.instanceName; this.instance.id = instance.instanceId; this.instance.integration = instance.integration; this.instance.number = instance.number; this.instance.token = instance.token; + this.instance.businessId = instance.businessId; this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, diff --git a/src/api/services/channels/whatsapp.business.service.ts b/src/api/services/channels/whatsapp.business.service.ts index d5fd9289..2ff8caf0 100644 --- a/src/api/services/channels/whatsapp.business.service.ts +++ b/src/api/services/channels/whatsapp.business.service.ts @@ -76,8 +76,7 @@ export class BusinessStartupService extends ChannelStartupService { const result = await axios.post(urlServer, message, { headers }); return result.data; } catch (e) { - this.logger.error(e); - return e.response.data; + return e.response?.data?.error; } } @@ -793,9 +792,9 @@ export class BusinessStartupService extends ChannelStartupService { } })(); - if (messageSent?.error?.message) { - this.logger.error(messageSent.error.message); - throw messageSent.error.message.toString(); + if (messageSent?.error_data) { + this.logger.error(messageSent); + return messageSent; } const messageRaw: any = { diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 3766b5d2..8706feb8 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -221,6 +221,7 @@ export class WAMonitoringService { integration: instanceData.integration, token: instanceData.token, number: instanceData.number, + businessId: instanceData.businessId, }); } else { instance = new BaileysStartupService( @@ -239,6 +240,7 @@ export class WAMonitoringService { integration: instanceData.integration, token: instanceData.token, number: instanceData.number, + businessId: instanceData.businessId, }); } @@ -267,6 +269,7 @@ export class WAMonitoringService { integration: instanceData.integration, token: instanceData.token, number: instanceData.number, + businessId: instanceData.businessId, }; this.setInstance(instance); @@ -294,6 +297,7 @@ export class WAMonitoringService { integration: instance.integration, token: instance.token, number: instance.number, + businessId: instance.businessId, }); }), ); @@ -317,6 +321,7 @@ export class WAMonitoringService { instanceName: instance.name, integration: instance.integration, token: instance.token, + businessId: instance.businessId, }); }), ); diff --git a/src/api/services/template.service.ts b/src/api/services/template.service.ts new file mode 100644 index 00000000..13a436f3 --- /dev/null +++ b/src/api/services/template.service.ts @@ -0,0 +1,105 @@ +import { Template } from '@prisma/client'; +import axios from 'axios'; + +import { ConfigService, WaBusiness } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { InstanceDto } from '../dto/instance.dto'; +import { TemplateDto } from '../dto/template.dto'; +import { PrismaRepository } from '../repository/repository.service'; +import { WAMonitoringService } from './monitor.service'; + +export class TemplateService { + constructor( + private readonly waMonitor: WAMonitoringService, + public readonly prismaRepository: PrismaRepository, + private readonly configService: ConfigService, + ) {} + + private readonly logger = new Logger(TemplateService.name); + + private businessId: string; + private token: string; + + public async find(instance: InstanceDto) { + const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance; + + if (!getInstance) { + throw new Error('Instance not found'); + } + + this.businessId = getInstance.businessId; + this.token = getInstance.token; + + const response = await this.requestTemplate({}, 'GET'); + + if (!response) { + throw new Error('Error to create template'); + } + + console.log(response); + + return response.data; + } + + public async create(instance: InstanceDto, data: TemplateDto): Promise