Skip to content

Commit

Permalink
Add support for managing WhatsApp templates via official API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dgcode-tec committed Jul 12, 2024
1 parent a145935 commit 26bddf3
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 12 additions & 0 deletions prisma/postgresql-schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ model Instance {
MessageUpdate MessageUpdate[]
TypebotSession TypebotSession[]
TypebotSetting TypebotSetting?
Template Template?
}

model Session {
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions src/api/controllers/instance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export class InstanceController {
integration,
token: hash,
number,
businessId,
});

instance.sendDataWebhook(Events.INSTANCE_CREATE, {
Expand Down
15 changes: 15 additions & 0 deletions src/api/controllers/template.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions src/api/dto/template.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class TemplateDto {
name: string;
category: string;
allowCategoryChange: boolean;
language: string;
components: any;
}
2 changes: 2 additions & 0 deletions src/api/routes/index.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions src/api/routes/template.router.ts
Original file line number Diff line number Diff line change
@@ -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<TemplateDto>({
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<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => templateController.findTemplate(instance),
});

res.status(HttpStatus.OK).json(response);
});
}

public readonly router = Router();
}
5 changes: 5 additions & 0 deletions src/api/server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -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);

Expand Down
4 changes: 3 additions & 1 deletion src/api/services/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 4 additions & 5 deletions src/api/services/channels/whatsapp.business.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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 = {
Expand Down
5 changes: 5 additions & 0 deletions src/api/services/monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export class WAMonitoringService {
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
});
} else {
instance = new BaileysStartupService(
Expand All @@ -239,6 +240,7 @@ export class WAMonitoringService {
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
});
}

Expand Down Expand Up @@ -267,6 +269,7 @@ export class WAMonitoringService {
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
};

this.setInstance(instance);
Expand Down Expand Up @@ -294,6 +297,7 @@ export class WAMonitoringService {
integration: instance.integration,
token: instance.token,
number: instance.number,
businessId: instance.businessId,
});
}),
);
Expand All @@ -317,6 +321,7 @@ export class WAMonitoringService {
instanceName: instance.name,
integration: instance.integration,
token: instance.token,
businessId: instance.businessId,
});
}),
);
Expand Down
105 changes: 105 additions & 0 deletions src/api/services/template.service.ts
Original file line number Diff line number Diff line change
@@ -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<Template> {
try {
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 postData = {
name: data.name,
category: data.category,
allow_category_change: data.allowCategoryChange,
language: data.language,
components: data.components,
};

const response = await this.requestTemplate(postData, 'POST');

if (!response) {
throw new Error('Error to create template');
}

console.log(response);

const template = await this.prismaRepository.template.create({
data: {
instanceId: getInstance.id,
templateId: response.id,
name: data.name,
language: data.language,
},
});

return template;
} catch (error) {
this.logger.error(error);
throw new Error('Error to create template');
}
}

private async requestTemplate(data: any, method: string) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
if (method === 'GET') {
const result = await axios.get(urlServer, { headers });
return result.data;
} else if (method === 'POST') {
const result = await axios.post(urlServer, data, { headers });
return result.data;
}
} catch (e) {
this.logger.error(e.response.data);
return null;
}
}
}
1 change: 1 addition & 0 deletions src/api/types/wa.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export declare namespace wa {
token?: string;
number?: string;
integration?: string;
businessId?: string;
};

export type LocalWebHook = {
Expand Down
35 changes: 35 additions & 0 deletions src/validate/template.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};

export const templateSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
name: { type: 'string' },
category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
allowCategoryChange: { type: 'boolean' },
language: { type: 'string' },
components: { type: 'array' },
},
required: ['name', 'category', 'language', 'components'],
...isNotEmpty('name', 'category', 'language', 'components'),
};
1 change: 1 addition & 0 deletions src/validate/validate.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from './label.schema';
export * from './message.schema';
export * from './proxy.schema';
export * from './settings.schema';
export * from './template.schema';
export * from './webhook.schema';
export * from './websocket.schema';

0 comments on commit 26bddf3

Please sign in to comment.